Browse Source

*** empty log message ***

David Rose 25 years ago
parent
commit
527b16a099

+ 28 - 0
pandatool/src/cvscopy/Sources.pp

@@ -0,0 +1,28 @@
+#begin lib_target
+  #define TARGET cvscopy
+  #define LOCAL_LIBS \
+    progbase pandatoolbase
+
+  #define OTHER_LIBS \
+    dconfig:c dtool:m
+
+  #define SOURCES \
+    cvsCopy.cxx cvsCopy.h cvsSourceDirectory.cxx cvsSourceDirectory.h \
+    cvsSourceTree.cxx cvsSourceTree.h
+
+  #define INSTALL_HEADERS \
+    cvsCopy.h
+
+#end lib_target
+
+#begin test_bin_target
+  #define TARGET testcopy
+  #define LOCAL_LIBS cvscopy
+
+  #define OTHER_LIBS \
+    dconfig:c dtool:m pystub
+
+  #define SOURCES \
+    testCopy.cxx testCopy.h
+
+#end test_bin_target

+ 278 - 0
pandatool/src/cvscopy/cvsCopy.cxx

@@ -0,0 +1,278 @@
+// Filename: cvsCopy.cxx
+// Created by:  drose (31Oct00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "cvsCopy.h"
+#include "cvsSourceDirectory.h"
+
+#include <notify.h>
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSCopy::Constructor
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+CVSCopy::
+CVSCopy() {
+  _model_dirname = ".";
+  _key_filename = "Sources.pp";
+  _cvs_binary = "cvs";
+  _model_dir = (CVSSourceDirectory *)NULL;
+  _map_dir = (CVSSourceDirectory *)NULL;
+
+  clear_runlines();
+  add_runline("[opts] file [file ... ]");
+
+  add_option
+    ("f", "", 80,
+     "Force copy to happen without any input from the user.  If a file "
+     "with the same name exists anywhere in the source hierarchy, it will "
+     "be overwritten without prompting; if a file does not yet exist, it "
+     "will be created in the directory named by -d or by -m, as appropriate.",
+     &CVSCopy::dispatch_none, &_force);
+
+  add_option
+    ("i", "", 80,
+     "The opposite of -f, this will prompt the user before each action.  "
+     "The default is only to prompt the user when an action is ambiguous.",
+     &CVSCopy::dispatch_none, &_interactive);
+
+  add_option
+    ("d", "dirname", 80, 
+     "Copy model files that are not already present somewhere in the tree "
+     "to the indicated directory.  The default is the current directory.",
+     &CVSCopy::dispatch_filename, &_got_model_dirname, &_model_dirname);
+
+  add_option
+    ("m", "dirname", 80, 
+     "Copy texture map files to the indicated directory.  The default "
+     "is src/maps from the root directory.",
+     &CVSCopy::dispatch_filename, &_got_map_dirname, &_map_dirname);
+
+  add_option
+    ("root", "dirname", 80, 
+     "Specify the root of the CVS source hierarchy.  The default is to "
+     "use the ppremake convention of locating the directory containing "
+     "Package.pp.",
+     &CVSCopy::dispatch_filename, &_got_root_dirname, &_root_dirname);
+
+  add_option
+    ("key", "filename", 80, 
+     "Specify the name of the file that must exist in each directory for "
+     "it to be considered part of the CVS source hierarchy.  The default "
+     "is the ppremake convention, \"Sources.pp\".  Other likely candidates "
+     "are \"CVS\" to search the entire CVS hierarchy, or \".\" to include "
+     "all subdirectories.",
+     &CVSCopy::dispatch_filename, NULL, &_key_filename);
+
+  add_option
+    ("nc", "", 80, 
+     "Do not attempt to add newly-created files to CVS.  The default "
+     "is to add them.",
+     &CVSCopy::dispatch_none, &_no_cvs);
+
+  add_option
+    ("cvs", "cvs_binary", 80, 
+     "Specify how to run the cvs program for adding newly-created files.  "
+     "The default is simply \"cvs\".",
+     &CVSCopy::dispatch_string, NULL, &_cvs_binary);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSCopy::import
+//       Access: Public
+//  Description: Checks for the given filename somewhere in the
+//               directory hierarchy, and chooses a place to import
+//               it.  Copies the file by calling copy_file().
+//
+//               Type is an integer number that is defined by the
+//               derivated class; CVSCopy simply passes it unchanged
+//               to copy_file().  It presumably gives the class a hint
+//               as to how the file should be copied.  Suggested_dir
+//               is the suggested directory in which to copy the file,
+//               if it does not already exist elsewhere.
+//
+//               On success, returns the CVSSourceDirectory it was
+//               actually copied to.  On failure, returns NULL.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSCopy::
+import(const Filename &source, int type, CVSSourceDirectory *suggested_dir) {
+  CopiedFiles::const_iterator ci;
+  ci = _copied_files.find(source);
+  if (ci != _copied_files.end()) {
+    // We have already copied this file.
+    return (*ci).second;
+  }
+
+  if (!source.exists()) {
+    cerr << "Source filename " << source << " does not exist!\n";
+    return (CVSSourceDirectory *)NULL;
+  }
+
+  string basename = source.get_basename();
+
+  CVSSourceDirectory *dir = 
+    _tree.choose_directory(basename, suggested_dir, _force, _interactive);
+  nassertr(dir != (CVSSourceDirectory *)NULL, dir);
+
+  Filename dest = dir->get_fullpath() + "/" + basename;
+
+  _copied_files[source] = dir;
+
+  bool new_file = !dest.exists();
+  if (!copy_file(source, dest, dir, type, new_file)) {
+    return (CVSSourceDirectory *)NULL;
+  }
+  if (new_file) {
+    create_file(dest);
+  }
+
+  return dir;
+}
+
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSCopy::handle_args
+//       Access: Protected, Virtual
+//  Description: Does something with the additional arguments on the
+//               command line (after all the -options have been
+//               parsed).  Returns true if the arguments are good,
+//               false otherwise.
+////////////////////////////////////////////////////////////////////
+bool CVSCopy::
+handle_args(Args &args) {
+  if (args.empty()) {
+    nout << "You must specify the file(s) to copy from on the command line.\n";
+    return false;
+  }
+
+  _source_files = args;
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSCopy::post_command_line
+//       Access: Protected, Virtual
+//  Description: This is called after the command line has been
+//               completely processed, and it gives the program a
+//               chance to do some last-minute processing and
+//               validation of the options and arguments.  It should
+//               return true if everything is fine, false if there is
+//               an error.
+////////////////////////////////////////////////////////////////////
+bool CVSCopy::
+post_command_line() {
+  if (!scan_hierarchy()) {
+    return false;
+  }
+
+  _model_dir = _tree.find_directory(_model_dirname);
+  if (_model_dir == (CVSSourceDirectory *)NULL) {
+    if (_got_model_dirname) {
+      nout << "Warning: model directory " << _model_dirname
+	   << " is not within the source hierarchy.\n";
+    }
+  }
+
+  if (_got_map_dirname) {
+    _map_dir = _tree.find_directory(_map_dirname);
+
+    if (_map_dir == (CVSSourceDirectory *)NULL) {
+      nout << "Warning: map directory " << _map_dirname
+	   << " is not within the source hierarchy.\n";
+    }
+
+  } else {
+    _map_dir = _tree.find_relpath("src/maps");
+
+    if (_map_dir == (CVSSourceDirectory *)NULL) {
+      nout << "Warning: no directory " << _tree.get_root_dirname()
+	   << "/src/maps.\n";
+      _map_dir = _model_dir;
+    }
+  }
+
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSCopy::create_file
+//       Access: Protected
+//  Description: Invokes CVS to add the indicated filename to the
+//               repository, if the user so requested.  Returns true
+//               if successful, false if there is an error.
+////////////////////////////////////////////////////////////////////
+bool CVSCopy::
+create_file(const Filename &filename) {
+  if (_no_cvs) {
+    return true;
+  }
+
+  if (!CVSSourceTree::temp_chdir(filename.get_dirname())) {
+    nout << "Invalid directory: " << filename.get_dirname() << "\n";
+    return false;
+  }
+
+  string command = _cvs_binary + " add " + filename.get_basename();
+  nout << command << "\n";
+  int result = system(command.c_str());
+
+  CVSSourceTree::restore_cwd();
+
+  if (result != 0) {
+    nout << "Failure invoking cvs.\n";
+    return false;
+  }
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSCopy::scan_hierarchy
+//       Access: Private
+//  Description: Starts the scan of the source hierarchy.  This
+//               identifies all of the files in the source hierarchy
+//               we're to copy these into, so we can guess where
+//               referenced files should be placed.  Returns true if
+//               everything is ok, false if there is an error.
+////////////////////////////////////////////////////////////////////
+bool CVSCopy::
+scan_hierarchy() {
+  if (!_got_root_dirname) {
+    // If we didn't get a root directory name, find the directory
+    // above this one that contains the file "Package.pp".
+    if (!scan_for_root(_model_dirname)) {
+      return false;
+    }
+  }
+
+  _tree.set_root(_root_dirname);
+  nout << "Root is " << _tree.get_root_fullpath() << "\n";
+
+  return _tree.scan(_key_filename);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSCopy::scan_for_root
+//       Access: Private
+//  Description: Searches for the root of the source directory by
+//               looking for the parent directory that contains
+//               "Package.pp".  Returns true on success, false on
+//               failure.
+////////////////////////////////////////////////////////////////////
+bool CVSCopy::
+scan_for_root(const string &dirname) {
+  Filename sources = dirname + "/Sources.pp";
+  if (!sources.exists()) {
+    nout << "Couldn't find " << sources << " in source directory.\n";
+    return false;
+  }
+  Filename package = dirname + "/Package.pp";
+  if (package.exists()) {
+    // Here's the root!
+    _root_dirname = dirname;
+    return true;
+  }
+
+  return scan_for_root(dirname + "/..");
+}

+ 67 - 0
pandatool/src/cvscopy/cvsCopy.h

@@ -0,0 +1,67 @@
+// Filename: cvsCopy.h
+// Created by:  drose (31Oct00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef CVSCOPY_H
+#define CVSCOPY_H
+
+#include <pandatoolbase.h>
+
+#include "cvsSourceTree.h"
+
+#include <programBase.h>
+#include <filename.h>
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : CVSCopy
+// Description : This is the base class for a family of programs that
+//               copy files, typically model files like .flt files and
+//               their associated textures, into a CVS-controlled
+//               source tree.
+////////////////////////////////////////////////////////////////////
+class CVSCopy : public ProgramBase {
+public:
+  CVSCopy();
+
+  CVSSourceDirectory *
+  import(const Filename &source, int type, CVSSourceDirectory *suggested_dir);
+
+protected:
+  virtual bool handle_args(Args &args);
+  virtual bool post_command_line();
+
+  virtual bool copy_file(Filename source, Filename dest,
+			 CVSSourceDirectory *dest_dir,
+			 int type, bool new_file)=0;
+  bool create_file(const Filename &filename);
+
+private:
+  bool scan_hierarchy(); 
+  bool scan_for_root(const string &dirname);
+  
+protected:
+  bool _force;
+  bool _interactive;
+  bool _got_model_dirname;
+  Filename _model_dirname;
+  bool _got_map_dirname;
+  Filename _map_dirname;
+  bool _got_root_dirname;
+  Filename _root_dirname;
+  Filename _key_filename;
+  bool _no_cvs;
+  string _cvs_binary;
+
+  typedef vector_string SourceFiles;
+  SourceFiles _source_files;
+
+  CVSSourceTree _tree;
+  CVSSourceDirectory *_model_dir;
+  CVSSourceDirectory *_map_dir;
+
+  typedef map<string, CVSSourceDirectory *> CopiedFiles;
+  CopiedFiles _copied_files;
+};
+
+#endif

+ 270 - 0
pandatool/src/cvscopy/cvsSourceDirectory.cxx

@@ -0,0 +1,270 @@
+// Filename: cvsSourceDirectory.cxx
+// Created by:  drose (31Oct00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "cvsSourceDirectory.h"
+#include "cvsSourceTree.h"
+
+#include <notify.h>
+
+#ifdef WIN32_VC
+// Windows uses a different API for scanning for files in a directory.
+#define WINDOWS_LEAN_AND_MEAN
+#include <windows.h>
+
+#else
+#include <sys/types.h>
+#include <dirent.h>
+#include <unistd.h>
+#endif
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::Constructor
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory::
+CVSSourceDirectory(CVSSourceTree *tree, CVSSourceDirectory *parent,
+		   const string &dirname) :
+  _tree(tree),
+  _parent(parent),
+  _dirname(dirname)
+{
+  if (_parent == (CVSSourceDirectory *)NULL) {
+    _depth = 0;
+  } else {
+    _depth = _parent->_depth + 1;
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::Destructor
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory::
+~CVSSourceDirectory() {
+  Children::iterator ci;
+  for (ci = _children.begin(); ci != _children.end(); ++ci) {
+    delete (*ci);
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::get_dirname
+//       Access: Public
+//  Description: Returns the local name of this particular directory.
+////////////////////////////////////////////////////////////////////
+string CVSSourceDirectory::
+get_dirname() const {
+  return _dirname;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::get_fullpath
+//       Access: Public
+//  Description: Returns the full pathname to this particular
+//               directory.
+////////////////////////////////////////////////////////////////////
+string CVSSourceDirectory::
+get_fullpath() const {
+  if (_parent == (CVSSourceDirectory *)NULL) {
+    return _tree->get_root_fullpath();
+  }
+  return _parent->get_fullpath() + "/" + _dirname;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::get_path
+//       Access: Public
+//  Description: Returns the relative pathname to this particular
+//               directory, as seen from the root of the tree.
+////////////////////////////////////////////////////////////////////
+string CVSSourceDirectory::
+get_path() const {
+  if (_parent == (CVSSourceDirectory *)NULL) {
+    return _dirname;
+  }
+  return _parent->get_path() + "/" + _dirname;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::get_rel_to
+//       Access: Public
+//  Description: Returns the relative path to the other directory from
+//               this one.  This does not include a trailing slash.
+////////////////////////////////////////////////////////////////////
+string CVSSourceDirectory::
+get_rel_to(const CVSSourceDirectory *other) const {
+  const CVSSourceDirectory *a = this;
+  const CVSSourceDirectory *b = other;
+
+  if (a == b) {
+    return ".";
+  }
+
+  string prefix, postfix;
+  while (a->_depth > b->_depth) {
+    prefix += "../";
+    a = a->_parent;
+    assert(a != (CVSSourceDirectory *)NULL);
+  }
+
+  while (b->_depth > a->_depth) {
+    postfix = b->_dirname + "/" + postfix;
+    b = b->_parent;
+    assert(b != (CVSSourceDirectory *)NULL);
+  }
+
+  while (a != b) {
+    prefix += "../";
+    postfix = b->_dirname + "/" + postfix;
+    a = a->_parent;
+    b = b->_parent;
+    assert(a != (CVSSourceDirectory *)NULL);
+    assert(b != (CVSSourceDirectory *)NULL);
+  }
+
+  string result = prefix + postfix;
+  assert(!result.empty());
+  return result.substr(0, result.length() - 1);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::get_num_children
+//       Access: Public
+//  Description: Returns the number of subdirectories below this
+//               directory.
+////////////////////////////////////////////////////////////////////
+int CVSSourceDirectory::
+get_num_children() const {
+  return _children.size();
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::get_child
+//       Access: Public
+//  Description: Returns the nth subdirectory below this directory.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceDirectory::
+get_child(int n) const {
+  nassertr(n >= 0 && n < (int)_children.size(), NULL);
+  return _children[n];
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::find_relpath
+//       Access: Public
+//  Description: Returns the source directory that corresponds to the
+//               given relative path from this directory, or NULL if
+//               there is no match.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceDirectory::
+find_relpath(const string &relpath) {
+  if (relpath.empty()) {
+    return this;
+  }
+
+  size_t slash = relpath.find('/');
+  string first = relpath.substr(0, slash);
+  string rest;
+  if (slash != string::npos) {
+    rest = relpath.substr(slash + 1);
+  }
+
+  if (first.empty() || first == ".") {
+    return find_relpath(rest);
+
+  } else if (first == "..") {
+    if (_parent != NULL) {
+      return _parent->find_relpath(rest);
+    }
+    // Tried to back out past the root directory.
+    return (CVSSourceDirectory *)NULL;
+  }
+
+  // Check for a child named "first".
+  Children::const_iterator ci;
+  for (ci = _children.begin(); ci != _children.end(); ++ci) {
+    if ((*ci)->get_dirname() == first) {
+      return (*ci)->find_relpath(rest);
+    }
+  }
+
+  // No match.
+  return (CVSSourceDirectory *)NULL;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::find_dirname
+//       Access: Public
+//  Description: Returns the source directory that corresponds to the
+//               given local directory name, or NULL if there is no
+//               match.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceDirectory::
+find_dirname(const string &dirname) {
+  if (dirname == _dirname) {
+    return this;
+  }
+
+  Children::const_iterator ci;
+  for (ci = _children.begin(); ci != _children.end(); ++ci) {
+    CVSSourceDirectory *result = (*ci)->find_dirname(dirname);
+    if (result != (CVSSourceDirectory *)NULL) {
+      return result;
+    }
+  }
+
+  return (CVSSourceDirectory *)NULL;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceDirectory::scan
+//       Access: Public
+//  Description: Recursively scans the contents of the source
+//               directory.  Fullpath is the full path name to the
+//               directory; key_filename is the name of a file that
+//               must exist in each subdirectory for it to be
+//               considered part of the hierarchy.  Returns true on
+//               success, false on failure.
+////////////////////////////////////////////////////////////////////
+bool CVSSourceDirectory::
+scan(const string &fullpath, const string &key_filename) {
+  DIR *root = opendir(fullpath.c_str());
+  if (root == (DIR *)NULL) {
+    cerr << "Unable to scan directory " << fullpath << "\n";
+    return false;
+  }
+
+  struct dirent *d;
+  d = readdir(root);
+  while (d != (struct dirent *)NULL) {
+    string filename = d->d_name;
+
+    if (!filename.empty() && filename[0] != '.') {
+      // Is this possibly a subdirectory name?
+      string next_path = fullpath + "/" + filename;
+      string key = next_path + "/" + key_filename;
+      if (access(key.c_str(), F_OK) == 0) {
+	CVSSourceDirectory *subdir = 
+	  new CVSSourceDirectory(_tree, this, filename);
+	_children.push_back(subdir);
+
+	if (!subdir->scan(next_path, key_filename)) {
+	  closedir(root);
+	  return false;
+	}
+
+      } else {
+	// It's not a subdirectory; call it a regular file.
+	_tree->add_file(filename, this);
+      }
+    }
+
+    d = readdir(root);
+  }
+  closedir(root);
+  return true;
+}

+ 52 - 0
pandatool/src/cvscopy/cvsSourceDirectory.h

@@ -0,0 +1,52 @@
+// Filename: cvsSourceDirectory.h
+// Created by:  drose (31Oct00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef CVSSOURCEDIRECTORY_H
+#define CVSSOURCEDIRECTORY_H
+
+#include <pandatoolbase.h>
+
+#include <vector>
+
+class CVSSourceTree;
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : CVSSourceDirectory
+// Description : This represents one particular directory in the
+//               hierarchy of source directory files.  We must scan
+//               the source directory to identify where the related
+//               files have previously been copied.
+////////////////////////////////////////////////////////////////////
+class CVSSourceDirectory {
+public:
+  CVSSourceDirectory(CVSSourceTree *tree, CVSSourceDirectory *parent,
+		     const string &dirname);
+  ~CVSSourceDirectory();
+
+  string get_dirname() const;
+  string get_fullpath() const;
+  string get_path() const;
+  string get_rel_to(const CVSSourceDirectory *other) const;
+
+  int get_num_children() const;
+  CVSSourceDirectory *get_child(int n) const;
+
+  CVSSourceDirectory *find_relpath(const string &relpath);
+  CVSSourceDirectory *find_dirname(const string &dirname);
+
+public:
+  bool scan(const string &prefix, const string &key_filename);
+
+private:
+  CVSSourceTree *_tree;
+  CVSSourceDirectory *_parent;
+  string _dirname;
+  int _depth;
+
+  typedef vector<CVSSourceDirectory *> Children;
+  Children _children;
+};
+
+#endif

+ 543 - 0
pandatool/src/cvscopy/cvsSourceTree.cxx

@@ -0,0 +1,543 @@
+// Filename: cvsSourceTree.cxx
+// Created by:  drose (31Oct00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "cvsSourceTree.h"
+#include "cvsSourceDirectory.h"
+
+#include <filename.h>
+#include <notify.h>
+
+#include <algorithm>
+#include <ctype.h>
+#include <stdio.h> // for perror
+#include <errno.h>
+
+bool CVSSourceTree::_got_start_fullpath = false;
+string CVSSourceTree::_start_fullpath;
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::Constructor
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+CVSSourceTree::
+CVSSourceTree() {
+  _root = (CVSSourceDirectory *)NULL;
+  _got_root_fullpath = false;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::Destructor
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+CVSSourceTree::
+~CVSSourceTree() {
+  if (_root != (CVSSourceDirectory *)NULL) {
+    delete _root;
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::set_root
+//       Access: Public
+//  Description: Sets the root of the source directory.  This must be
+//               called before scan(), and should not be called more
+//               than once.
+////////////////////////////////////////////////////////////////////
+void CVSSourceTree::
+set_root(const string &root_path) {
+  nassertv(_path.empty());
+  _path = root_path;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::scan
+//       Access: Public
+//  Description: Scans the complete source directory starting at the
+//               indicated pathname.  It is an error to call this more
+//               than once.  Returns true on success, false if there
+//               is an error.
+////////////////////////////////////////////////////////////////////
+bool CVSSourceTree::
+scan(const string &key_filename) {
+  nassertr(_root == (CVSSourceDirectory *)NULL, false);
+  Filename root_fullpath = get_root_fullpath();
+  _root = new CVSSourceDirectory(this, NULL, root_fullpath.get_basename());
+  return _root->scan(_path, key_filename);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::get_root
+//       Access: Public
+//  Description: Returns the root directory of the hierarchy.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceTree::
+get_root() const {
+  return _root;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::find_directory
+//       Access: Public
+//  Description: Returns the source directory that corresponds to the
+//               given path, or NULL if there is no such directory in
+//               the source tree.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceTree::
+find_directory(const string &path) {
+  string root_fullpath = get_root_fullpath();
+  string fullpath = get_actual_fullpath(path);
+
+  // path is a subdirectory within the source hierarchy if and only if
+  // root_fullpath is an initial prefix of fullpath.
+  if (root_fullpath.length() > fullpath.length() ||
+      fullpath.substr(0, root_fullpath.length()) != root_fullpath) {
+    // Nope!
+    return (CVSSourceDirectory *)NULL;
+  }
+
+  // The relative name is the part of fullpath not in root_fullpath.
+  string relpath = fullpath.substr(root_fullpath.length());
+
+  return _root->find_relpath(relpath);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::find_relpath
+//       Access: Public
+//  Description: Returns the source directory that corresponds to the
+//               given relative path from the root, or NULL if there
+//               is no match.  The relative path may or may not
+//               include the name of the root directory itself.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceTree::
+find_relpath(const string &relpath) {
+  CVSSourceDirectory *result = _root->find_relpath(relpath);
+  if (result != (CVSSourceDirectory *)NULL) {
+    return result;
+  }
+
+  // Check for the root dirname at the front of the path, and remove
+  // it if it's there.
+  size_t slash = relpath.find('/');
+  string first = relpath.substr(0, slash);
+  string rest;
+  if (slash != string::npos) {
+    rest = relpath.substr(slash + 1);
+  }
+
+  if (first == _root->get_dirname()) {
+    return _root->find_relpath(rest);
+  }
+
+  return (CVSSourceDirectory *)NULL;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::find_dirname
+//       Access: Public
+//  Description: Returns the source directory that corresponds to the
+//               given local directory name, or NULL if there
+//               is no match.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceTree::
+find_dirname(const string &dirname) {
+  return _root->find_dirname(dirname);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::choose_directory
+//       Access: Public
+//  Description: Determines where an externally referenced model file
+//               of the indicated name should go.  It does this by
+//               looking for an existing model file of the same name;
+//               if a matching model is not found, or if multiple
+//               matching files are found, prompts the user for the
+//               directory, or uses suggested_dir.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceTree::
+choose_directory(const string &filename, CVSSourceDirectory *suggested_dir,
+		 bool force, bool interactive) {
+  static Directories empty_dirs;
+
+  Filenames::const_iterator fi;
+  fi = _filenames.find(filename);
+  if (fi != _filenames.end()) {
+    // The filename already exists somewhere.
+    const Directories &dirs = (*fi).second;
+
+    return prompt_user(filename, suggested_dir, dirs,
+		       force, interactive);
+  }
+
+  // Now we have to prompt the user for a suitable place to put it.
+  return prompt_user(filename, suggested_dir, empty_dirs,
+		     force, interactive);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::get_root_fullpath
+//       Access: Public
+//  Description: Returns the full path from the root to the top of
+//               the source hierarchy.
+////////////////////////////////////////////////////////////////////
+string CVSSourceTree::
+get_root_fullpath() {
+  nassertr(!_path.empty(), string());
+  if (!_got_root_fullpath) {
+    _root_fullpath = get_actual_fullpath(_path);
+    _got_root_fullpath = true;
+  }
+  return _root_fullpath;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::get_root_dirname
+//       Access: Public
+//  Description: Returns the local directory name of the root of the
+//               tree.
+////////////////////////////////////////////////////////////////////
+string CVSSourceTree::
+get_root_dirname() const {
+  nassertr(_root != (CVSSourceDirectory *)NULL, string());
+  return _root->get_dirname();
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::add_file
+//       Access: Public
+//  Description: Adds a new file to the set of known files.  This is
+//               normally called from CVSSourceDirectory::scan() and
+//               should not be called directly by the user.
+////////////////////////////////////////////////////////////////////
+void CVSSourceTree::
+add_file(const string &filename, CVSSourceDirectory *dir) {
+  _filenames[filename].push_back(dir);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::temp_chdir
+//       Access: Public, Static
+//  Description: Temporarily changes the current directory to the
+//               named path.  Returns true on success, false on
+//               failure.  Call restore_cwd() to restore to the
+//               original directory later.
+////////////////////////////////////////////////////////////////////
+bool CVSSourceTree::
+temp_chdir(const string &path) {
+  // We have to call this first to guarantee that we have already
+  // determined our starting directory.
+  get_start_fullpath();
+
+  if (chdir(path.c_str()) < 0) {
+    return false;
+  }
+  return true;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::restore_cwd
+//       Access: Public, Static
+//  Description: Restores the current directory after changing it from
+//               temp_chdir().
+////////////////////////////////////////////////////////////////////
+void CVSSourceTree::
+restore_cwd() {
+  string start_fullpath = get_start_fullpath();
+
+  if (chdir(start_fullpath.c_str()) < 0) {
+    // Hey!  We can't get back to the directory we started from!
+    perror(start_fullpath.c_str());
+    cerr << "Can't continue, aborting.\n";
+    exit(1);
+  }
+}
+
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::prompt_user
+//       Access: Private
+//  Description: Prompts the user, if necessary, to choose a directory
+//               to import the given file into.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceTree::
+prompt_user(const string &filename, CVSSourceDirectory *suggested_dir,
+	    const CVSSourceTree::Directories &dirs,
+	    bool force, bool interactive) {
+  if (dirs.size() == 1) {
+    // The file already exists in exactly one place.
+    if (interactive) {
+      return dirs[0];
+    }
+    CVSSourceDirectory *result = ask_existing(filename, dirs[0]);
+    if (result != (CVSSourceDirectory *)NULL) {
+      return result;
+    }
+
+  } else if (dirs.size() > 1) {
+    // The file already exists in multiple places.
+    if (force && !interactive) {
+      return dirs[0];
+    }
+    CVSSourceDirectory *result = ask_existing(filename, dirs, suggested_dir);
+    if (result != (CVSSourceDirectory *)NULL) {
+      return result;
+    }
+  }
+
+  // The file does not already exist, or the user declined to replace
+  // an existing file.
+  if (force && !interactive) {
+    return suggested_dir;
+  }
+
+  if (find(dirs.begin(), dirs.end(), suggested_dir) == dirs.end()) {
+    CVSSourceDirectory *result = ask_new(filename, suggested_dir);
+    if (result != (CVSSourceDirectory *)NULL) {
+      return result;
+    }
+  }
+
+  // Ask the user where the damn thing should go.
+  return ask_any(filename);
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::ask_existing
+//       Access: Private
+//  Description: Asks the user if he wants to replace an existing
+//               file.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceTree::
+ask_existing(const string &filename, CVSSourceDirectory *dir) {
+  while (true) {
+    nout << filename << " found in tree at " 
+	 << dir->get_path() + "/" + filename << ".\n";
+    string result = prompt("Overwrite this file (y/n)? ");
+    nassertr(!result.empty(), (CVSSourceDirectory *)NULL);
+    if (result.size() == 1) {
+      if (tolower(result[0]) == 'y') {
+	return dir;
+      } else if (tolower(result[0]) == 'n') {
+	return NULL;
+      }
+    }
+
+    nout << "*** Invalid response: " << result << "\n\n";
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::ask_existing
+//       Access: Private
+//  Description: Asks the user which of several existing files he
+//               wants to replace.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceTree::
+ask_existing(const string &filename, const CVSSourceTree::Directories &dirs,
+	     CVSSourceDirectory *suggested_dir) {
+  while (true) {
+    nout << filename << " found in tree at more than one place:\n";
+
+    bool any_suggested = false;
+    for (int i = 0; i < (int)dirs.size(); i++) {
+      nout << "  " << (i + 1) << ". " 
+	   << dirs[i]->get_path() + "/" + filename << "\n";
+      if (dirs[i] == suggested_dir) {
+	any_suggested = true;
+      }
+    }
+
+    int next_option = dirs.size() + 1;
+    int suggested_option = -1;
+
+    if (!any_suggested) {
+      suggested_option = next_option;
+      next_option++;
+      nout << "\n" << suggested_option
+	   << ". create " 
+	   << suggested_dir->get_path() + "/" + filename
+	   << "\n";
+    }
+
+    int other_option = next_option;
+    nout << other_option << ". Other\n";
+
+    string result = prompt("Choose an option: ");
+    nassertr(!result.empty(), (CVSSourceDirectory *)NULL);
+    const char *nptr = result.c_str();
+    char *endptr;
+    int option = strtol(nptr, &endptr, 10);
+    if (*endptr == '\0') {
+      if (option >= 1 && option <= (int)dirs.size()) {
+	return dirs[option - 1];
+
+      } else if (option == suggested_option) {
+	return suggested_dir;
+
+      } else if (option == other_option) {
+	return NULL;
+      }
+    }
+
+    nout << "*** Invalid response: " << result << "\n\n";
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::ask_new
+//       Access: Private
+//  Description: Asks the user if he wants to replace an existing
+//               file.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceTree::
+ask_new(const string &filename, CVSSourceDirectory *dir) {
+  while (true) {
+    nout << filename << " will be created at "
+	 << dir->get_path() + "/" + filename << ".\n";
+    string result = prompt("Create this file (y/n)? ");
+    nassertr(!result.empty(), (CVSSourceDirectory *)NULL);
+    if (result.size() == 1) {
+      if (tolower(result[0]) == 'y') {
+	return dir;
+      } else if (tolower(result[0]) == 'n') {
+	return NULL;
+      }
+    }
+
+    nout << "*** Invalid response: " << result << "\n\n";
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::ask_any
+//       Access: Private
+//  Description: Asks the user to type in the name of the directory in
+//               which to store the file.
+////////////////////////////////////////////////////////////////////
+CVSSourceDirectory *CVSSourceTree::
+ask_any(const string &filename) {
+  while (true) {
+    string result =
+      prompt("Enter the name of the directory to copy " + filename + " to: ");
+    nassertr(!result.empty(), (CVSSourceDirectory *)NULL);
+
+    // The user might enter a fully-qualified path to the directory,
+    // or a relative path from the root (with or without the root's
+    // dirname), or the dirname of the particular directory.
+    CVSSourceDirectory *dir = find_directory(result);
+    if (dir == (CVSSourceDirectory *)NULL) {
+      dir = find_relpath(result);
+    }
+    if (dir == (CVSSourceDirectory *)NULL) {
+      dir = find_dirname(result);
+    }
+
+    if (dir != (CVSSourceDirectory *)NULL) {
+      return dir;
+    }
+
+    nout << "*** Not a valid directory name: " << result << "\n\n";
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::prompt
+//       Access: Private
+//  Description: Issues a prompt to the user and waits for a typed
+//               response.  Returns the response (which will not be
+//               empty).
+////////////////////////////////////////////////////////////////////
+string CVSSourceTree::
+prompt(const string &message) {
+  nout << flush;
+  while (true) {
+    cerr << message << flush;
+    string response;
+    getline(cin, response);
+
+    // Remove leading and trailing whitespace.
+    size_t p = 0;
+    while (p < response.length() && isspace(response[p])) {
+      p++;
+    }
+    
+    size_t q = response.length();
+    while (q > p && isspace(response[q - 1])) {
+      q--;
+    }
+
+    if (q > p) {
+      return response.substr(p, q - p);
+    }
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::get_actual_fullpath
+//       Access: Private, Static
+//  Description: Determines the actual full path from the root to the
+//               named directory.  It does this essentially by cd'ing
+//               to the directory and doing pwd, then cd'ing back.
+//               Returns the empty string if the directory is invalid
+//               or cannot be cd'ed into.
+////////////////////////////////////////////////////////////////////
+string CVSSourceTree::
+get_actual_fullpath(const string &path) {
+  if (!temp_chdir(path)) {
+    return string();
+  }
+
+  string cwd = get_cwd();
+  restore_cwd();
+
+  return cwd;
+}
+
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::get_start_fullpath
+//       Access: Private, Static
+//  Description: Returns the full path from the root to the directory
+//               in which the user started the program.
+////////////////////////////////////////////////////////////////////
+string CVSSourceTree::
+get_start_fullpath() {
+  if (!_got_start_fullpath) {
+    _start_fullpath = get_cwd();
+    _got_start_fullpath = true;
+  }
+  return _start_fullpath;
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: CVSSourceTree::get_cwd
+//       Access: Private, Static
+//  Description: Calls the system getcwd(), automatically allocating a
+//               large enough string.
+////////////////////////////////////////////////////////////////////
+string CVSSourceTree::
+get_cwd() {
+  static size_t bufsize = 1024;
+  static char *buffer = NULL;
+
+  if (buffer == (char *)NULL) {
+    buffer = new char[bufsize];
+  }
+
+  while (getcwd(buffer, bufsize) == (char *)NULL) {
+    if (errno != ERANGE) {
+      perror("getcwd");
+      return string();
+    }
+    delete[] buffer;
+    bufsize = bufsize * 2;
+    buffer = new char[bufsize];
+    nassertr(buffer != (char *)NULL, string());
+  }
+
+  return string(buffer);
+}

+ 81 - 0
pandatool/src/cvscopy/cvsSourceTree.h

@@ -0,0 +1,81 @@
+// Filename: cvsSourceTree.h
+// Created by:  drose (31Oct00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef CVSSOURCETREE_H
+#define CVSSOURCETREE_H
+
+#include <pandatoolbase.h>
+
+#include <vector>
+#include <map>
+
+class CVSSourceDirectory;
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : CVSSourceTree
+// Description : This represents the root of the tree of source
+//               directory files.
+////////////////////////////////////////////////////////////////////
+class CVSSourceTree {
+public:
+  CVSSourceTree();
+  ~CVSSourceTree();
+
+  void set_root(const string &root_path);
+  bool scan(const string &key_filename);
+
+  CVSSourceDirectory *get_root() const;
+  CVSSourceDirectory *find_directory(const string &path);
+  CVSSourceDirectory *find_relpath(const string &relpath);
+  CVSSourceDirectory *find_dirname(const string &dirname);
+
+  CVSSourceDirectory *choose_directory(const string &filename,
+				       CVSSourceDirectory *suggested_dir,
+				       bool force, bool interactive);
+
+  string get_root_fullpath();
+  string get_root_dirname() const;
+
+  static bool temp_chdir(const string &path);
+  static void restore_cwd();
+
+public:
+  void add_file(const string &filename, CVSSourceDirectory *dir);
+
+private:
+  typedef vector<CVSSourceDirectory *> Directories;
+
+  CVSSourceDirectory *
+  prompt_user(const string &filename, CVSSourceDirectory *suggested_dir,
+	      const Directories &dirs, bool force, bool interactive);
+
+  CVSSourceDirectory *ask_existing(const string &filename, 
+				   CVSSourceDirectory *dir);
+  CVSSourceDirectory *ask_existing(const string &filename,
+				   const Directories &dirs,
+				   CVSSourceDirectory *suggested_dir);
+  CVSSourceDirectory *ask_new(const string &filename, CVSSourceDirectory *dir);
+  CVSSourceDirectory *ask_any(const string &filename);
+
+  string prompt(const string &message);
+
+  static string get_actual_fullpath(const string &path);
+  static string get_start_fullpath();
+  static string get_cwd();
+
+private:
+  string _path;
+  CVSSourceDirectory *_root;
+
+  typedef map<string, Directories> Filenames;
+  Filenames _filenames;
+
+  static bool _got_start_fullpath;
+  static string _start_fullpath;
+  bool _got_root_fullpath;
+  string _root_fullpath;
+};
+
+#endif

+ 94 - 0
pandatool/src/cvscopy/testCopy.cxx

@@ -0,0 +1,94 @@
+// Filename: testCopy.cxx
+// Created by:  drose (31Oct00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#include "testCopy.h"
+#include "cvsSourceDirectory.h"
+
+////////////////////////////////////////////////////////////////////
+//     Function: TestCopy::Constructor
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+TestCopy::
+TestCopy() {
+  set_program_description
+    ("This program copies one or more files into a CVS source hierarchy.  "
+     "Rather than copying the named files immediately into the current "
+     "directory, it first scans the entire source hierarchy, identifying all "
+     "the already-existing files.  If the named file to copy matches the "
+     "name of an already-existing file in the current directory or elsewhere "
+     "in the hierarchy, that file is overwritten.\n\n"
+
+     "This is primarily useful as a test program for libcvscopy.");
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: TestCopy::run
+//       Access: Public
+//  Description: 
+////////////////////////////////////////////////////////////////////
+void TestCopy::
+run() {
+  SourceFiles::iterator fi;
+  for (fi = _source_files.begin(); fi != _source_files.end(); ++fi) {
+    CVSSourceDirectory *dest = import(*fi, 0, _model_dir);
+    if (dest == (CVSSourceDirectory *)NULL) {
+      exit(1);
+    }
+  }
+}
+
+////////////////////////////////////////////////////////////////////
+//     Function: TestCopy::copy_file
+//       Access: Protected, Virtual
+//  Description: Called by import() if the timestamps indicate that a
+//               file needs to be copied.  This does the actual copy
+//               of a file from source to destination.  If new_file is
+//               true, then dest does not already exist.
+////////////////////////////////////////////////////////////////////
+bool TestCopy::
+copy_file(Filename source, Filename dest,
+	  CVSSourceDirectory *dir, int type, bool new_file) {
+  source.set_binary();
+  dest.set_binary();
+
+  ifstream in;
+  ofstream out;
+  if (!source.open_read(in)) {
+    cerr << "Cannot read " << source << "\n";
+    return false;
+  }
+
+  if (!dest.open_write(out)) {
+    cerr << "Cannot write " << dest << "\n";
+    return false;
+  }
+
+  int c;
+  c = in.get();
+  while (!in.eof() && !in.fail() && !out.fail()) {
+    out.put(c);
+    c = in.get();
+  }
+
+  if (in.fail()) {
+    cerr << "Error reading " << source << "\n";
+    return false;
+  }
+  if (out.fail()) {
+    cerr << "Error writing " << dest << "\n";
+    return false;
+  }
+  
+  return true;
+}
+
+
+int main(int argc, char *argv[]) {
+  TestCopy prog;
+  prog.parse_command_line(argc, argv);
+  prog.run();
+  return 0;
+}

+ 29 - 0
pandatool/src/cvscopy/testCopy.h

@@ -0,0 +1,29 @@
+// Filename: testCopy.h
+// Created by:  drose (31Oct00)
+// 
+////////////////////////////////////////////////////////////////////
+
+#ifndef TESTCOPY_H
+#define TESTCOPY_H
+
+#include <pandatoolbase.h>
+
+#include "cvsCopy.h"
+
+////////////////////////////////////////////////////////////////////
+// 	 Class : TestCopy
+// Description : A program to copy ordinary files into the cvs tree.
+//               Mainly to test CVSCopy.
+////////////////////////////////////////////////////////////////////
+class TestCopy : public CVSCopy {
+public:
+  TestCopy();
+
+  void run();
+
+protected:
+  virtual bool copy_file(Filename source, Filename dest,
+			 CVSSourceDirectory *dir, int type, bool new_file);
+};
+
+#endif

+ 29 - 7
pandatool/src/progbase/programBase.cxx

@@ -73,6 +73,7 @@ ProgramBase() {
 
   _next_sequence = 0;
   _sorted_options = false;
+  _last_newline = false;
   _got_terminal_width = false;
   _got_option_indent = false;
 
@@ -171,7 +172,8 @@ show_text(const string &prefix, int indent_width, string text) {
   // This is correct!  It goes go to cerr, not to nout.  Sending it to
   // nout would be cyclic, since nout is redefined to map back through
   // this function.
-  format_text(cerr, prefix, indent_width, text, _terminal_width);
+  format_text(cerr, _last_newline, 
+	      prefix, indent_width, text, _terminal_width);
 }
 
 ////////////////////////////////////////////////////////////////////
@@ -679,12 +681,19 @@ handle_help_option(const string &, const string &, void *) {
 //
 //               An embedded newline character ('\n') forces a line
 //               break, while an embedded carriage-return character
-//               ('\r') marks a paragraph break, which is usually
-//               printed as a blank line.  Redundant newline and
-//               carriage-return characters are generally ignored.
+//               ('\r'), or two or more consecutive newlines, marks a
+//               paragraph break, which is usually printed as a blank
+//               line.  Redundant newline and carriage-return
+//               characters are generally ignored.
+//
+//               The flag last_newline should be initialized to false
+//               for the first call to format_text, and then preserved
+//               for future calls; it tracks the state of trailing
+//               newline characters between calls so we can correctly
+//               identify doubled newlines.
 ////////////////////////////////////////////////////////////////////
 void ProgramBase::
-format_text(ostream &out,
+format_text(ostream &out, bool &last_newline,
 	    const string &prefix, int indent_width,
 	    const string &text, int line_width) {
   indent_width = min(indent_width, line_width - 20);
@@ -705,7 +714,9 @@ format_text(ostream &out,
 
   // Skip any initial whitespace and newlines.
   while (p < text.length() && isspace(text[p])) {
-    if (text[p] == '\r') {
+    if (text[p] == '\r' ||
+	(p > 0 && text[p] == '\n' && text[p - 1] == '\n') ||
+ 	(p == 0 && text[p] == '\n' && last_newline)) {
       if (!initial_break) {
 	// Here's an initial paragraph break, however.
 	out << "\n";
@@ -724,6 +735,8 @@ format_text(ostream &out,
     p++;
   }
 
+  last_newline = (!text.empty() && text[text.length() - 1] == '\n');
+
   while (p < text.length()) {
     // Look for the paragraph or line break--the next newline
     // character, if any.
@@ -731,8 +744,11 @@ format_text(ostream &out,
     bool is_paragraph_break = false;
     if (par == string::npos) {
       par = text.length();
+      /*
+	This shouldn't be necessary.
     } else {
       is_paragraph_break = (text[par] == '\r');
+      */
     }
 
     indent(out, indent_amount);
@@ -771,7 +787,8 @@ format_text(ostream &out,
 
     // Skip additional whitespace between the lines.
     while (p < text.length() && isspace(text[p])) {
-      if (text[p] == '\r') {
+      if (text[p] == '\r' ||
+	  (p > 0 && text[p] == '\n' && text[p - 1] == '\n')) {
 	is_paragraph_break = true;
       }
       p++;
@@ -780,6 +797,11 @@ format_text(ostream &out,
     if (eol == par && is_paragraph_break) {
       // Print the paragraph break as a blank line.
       out << "\n";
+      if (p >= text.length()) {
+	// If we end on a paragraph break, don't try to insert a new
+	// one in the next pass.
+	last_newline = false;
+      }
     }
 
     indent_amount = indent_width;

+ 2 - 1
pandatool/src/progbase/programBase.h

@@ -68,7 +68,7 @@ protected:
 
   bool handle_help_option(const string &opt, const string &arg, void *);
 
-  static void format_text(ostream &out,
+  static void format_text(ostream &out, bool &last_newline,
 			  const string &prefix, int indent_width,
 			  const string &text, int line_width);
 
@@ -107,6 +107,7 @@ private:
   typedef map<string, string> GotOptions;
   GotOptions _got_options;
 
+  bool _last_newline;
   int _terminal_width;
   bool _got_terminal_width;
   int _option_indent;