/* -*-Mode: C++;-*- * PRCS - The Project Revision Control System * Copyright (C) 1997 Josh MacDonald * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * * $Id: checkin.cc 1.22.1.2.1.9.1.23.1.1.1.6.1.15.1.2.1.49 Sun, 09 May 2004 18:21:12 -0700 jmacd $ */ #include "prcs.h" #include "setkeys.h" #include "projdesc.h" #include "fileent.h" #include "repository.h" #include "vc.h" #include "checkout.h" #include "checkin.h" #include "misc.h" #include "memseg.h" #include "rebuild.h" #include "system.h" #include "quick.h" #include "syscmd.h" #include "diff.h" static const char rlog_header[] = "checked in by PRCS version %d %d %d\n"; static PrVoidError write_new_prjfile(ProjectDescriptor* project, ProjectVersionData *parent_data); static PrVoidError checkin_each_file(ProjectDescriptor*); static PrVoidError maybe_query_user(ProjectDescriptor*); static PrVoidError slow_eliminate_prepare(ProjectDescriptor*); static PrVoidError checkin_cleanup_handler(void* data, bool); static PrVoidError compute_partial_modified(ProjectDescriptor *project, ProjectDescriptor *pred_project); PrPrcsExitStatusError checkin_command() { ProjectVersionData *new_project_data; /* New version data. */ ProjectDescriptor *project; /* The working project. */ ProjectVersionData *pred_project_data = NULL; /* The predecessor project, if different */ ProjectDescriptor *pred_project = NULL; /* from working Project-Version. */ Return_if_fail(project << read_project_file(cmd_root_project_full_name, cmd_root_project_file_path, true, (ProjectReadData)(KeepMergeParents))); install_cleanup_handler(checkin_cleanup_handler, project); Return_if_fail(project->init_repository_entry(cmd_root_project_name, true, true)); /* remove files that were not named on the command line */ eliminate_unnamed_files(project); /* warn the user about filenames on the command line that didn't match any files */ Return_if_fail(warn_unused_files(true)); /* select the new version and possibly warn the user of unwise checkins */ Return_if_fail(new_project_data << project->resolve_checkin_version(cmd_version_specifier_major, &pred_project_data)); prcsinfo << "Checking in project " << squote(cmd_root_project_name) << " version " << new_project_data << dotendl; /* check that no unselected files have empty descriptors and that all selected * files exist */ Return_if_fail(eliminate_working_files(project, QueryUserRemoveFromCommandLine)); Return_if_fail(check_project(project)); /* if the auxiliary database is present, read it and eliminate files that * have not been touched since the last checkout or rekey */ project->read_quick_elim(); project->quick_elim_unmodified(); if (pred_project_data) { Return_if_fail (pred_project << project->repository_entry()-> checkout_prj_file (project->project_full_name(), pred_project_data->rcs_version(), KeepNothing)); } /* now eliminate files that have not changed by reading and unkeying files, * comparing the unkeyed length, then computing their checksum and comparing * the unkeyed checksum. with the unkeyed file in memory, prepare the repository * file (possibly uncompress), and write it out into the temporary location * in the repository. */ Return_if_fail(slow_eliminate_prepare(project)); /* report changes to the user, and also make partial checkins use * the pred_project's versions for files omitted from the command * line of this checkin. */ Return_if_fail(compute_partial_modified(project, pred_project)); if(option_report_actions) { return ExitSuccess; } /* check if the user wants to enter a log. */ Return_if_fail(maybe_query_user(project)); project->repository_entry()->Rep_log() << "Checking in project version " << new_project_data << dotendl; Return_if_fail(checkin_each_file(project)); foreach_fileent(fe_ptr, project) { FileEntry* fe = *fe_ptr; if(fe->real_on_cmd_line()) project->quick_elim_update_fe(fe); } project->quick_elim_update(); Return_if_fail(project->update_attributes(new_project_data)); Return_if_fail(write_new_prjfile(project, new_project_data)); project->repository_entry()->Rep_log() << "Finished checking in" << dotendl; return ExitSuccess; } static PrVoidError compute_partial_modified(ProjectDescriptor *project, ProjectDescriptor *pred_project) { /* This shares some structure with changes_command, not all of it * though. We know that pred_project is not deleted. */ bool project_modified = false; /* if pred_project is non-null, this function makes an ugly use of * the on_command_line() status bit to mean something else: * whether the preceding project versions' file was deleted. */ foreach_fileent(fe_ptr, project) { FileEntry *fe = *fe_ptr; const char *name = fe->working_path(); const char *lname; bool modified = false; bool new_file = false; FileEntry *pred_fe = NULL; if (pred_project) { Return_if_fail (pred_fe << pred_project->match_fileent (fe)); } if (pred_fe) { pred_fe->set_on_command_line (false); } else { project_modified = true; new_file = true; if (option_long_format) { prcsoutput << format_type (fe->file_type(), true) << " " << squote (name) << " was added" << dotendl; } } if (pred_fe && fe->file_type() != pred_fe->file_type()) { project_modified = true; if (option_long_format) { prcsoutput << "File " << squote (name) << " changes from type " << format_type (pred_fe->file_type()) << " to " << format_type (fe->file_type()) << dotendl; } } if (pred_fe) { const char* a = pred_fe->working_path(), *b = fe->working_path(); if (!pathname_equal (a, b)) { project_modified = true; if (option_long_format) { prcsoutput << "File " << squote (name) << " is renamed from " << squote (pred_fe->working_path()) << dotendl; } } } if (!fe->on_command_line()) { if ((pred_fe != NULL) && (pred_fe->file_type () == fe->file_type ()) && (fe->file_type () == RealFile) && (strcmp (fe->descriptor_name(), pred_fe->descriptor_name()) != 0 || strcmp (fe->descriptor_version_number(), pred_fe->descriptor_version_number()) != 0)) { if (option_long_format) { prcsoutput << "Non-selected file " << squote (name) << " assumes pre-existing version" << dotendl; } /* Note: This is the side effect of this call. */ fe->set_descriptor_name (pred_fe->descriptor_name ()); fe->set_version_number (pred_fe->descriptor_version_number ()); } continue; } switch (fe->file_type()) { case Directory: break; case SymLink: Return_if_fail (lname << read_sym_link (name)); fe->set_link_name (lname); if (!pred_fe || !pred_fe->link_name() || strcmp (lname, pred_fe->link_name()) != 0) { modified = true; } if (!new_file) { if (!modified && option_really_long_format) { prcsoutput << "Symlink " << squote(name) << " is unmodified" << dotendl; } else if (modified && option_long_format) { prcsoutput << "Symlink " << squote(name) << " is modified" << dotendl; } } break; case RealFile: if (!fe->unmodified()) { modified = true; if (option_long_format && !new_file) { prcsoutput << "New version of file " << squote(name) << dotendl; } } if (pred_fe && fe->descriptor_name() && fe->file_type() == pred_fe->file_type()) { if (strcmp (fe->descriptor_name(), pred_fe->descriptor_name()) != 0 || strcmp (fe->descriptor_version_number(), pred_fe->descriptor_version_number()) != 0) { modified = true; if (option_long_format) { prcsoutput << "File " << squote (name) << " changes versions" << dotendl; } } } if (fe->file_mode() != fe->stat_permissions()) { if (option_long_format && !new_file) { prcsoutput << "File " << squote(name) << " has new mode" << dotendl; } modified = true; fe->set_file_mode (fe->stat_permissions()); } if (!new_file && !modified && option_really_long_format && option_long_format) { prcsoutput << "File " << squote(name) << " is unmodified" << dotendl; } break; } project_modified |= modified; } if (pred_project) { foreach_fileent (fe_ptr, pred_project) { FileEntry *fe = *fe_ptr; if (fe->on_command_line()) { project_modified = true; if (option_long_format) { prcsoutput << "File " << squote(fe->working_path()) << " was deleted" << dotendl; } } } } if(!project_modified) { prcsquery << "No modifications were found. " << force("Aborting checkin") << report("Abort checkin") << option('n', "Continue, with no modified files in this version") << deffail('y') << query("Abort"); Return_if_fail(prcsquery.result()); } return NoError; } PrVoidError ProjectDescriptor::update_attributes(ProjectVersionData *new_project_data) { /* The Versions. */ if (strcmp (*project_version_minor(), "0") != 0) { parent_version_major()->assign(*project_version_major()); parent_version_minor()->assign(*project_version_minor()); } project_version_major()->assign(new_project_data->prcs_major()); project_version_minor()->assign(new_project_data->prcs_minor()); /* the Checkin-Time attribute */ checkin_time()->sprintf("%s", get_utc_time()); /* the Logs */ if (option_version_log) { if (new_version_log()->length() != 0) { prcsquery << "You have supplied a version log on the command line, but the project file contains a new version log already. " << force("Overriding") << report("Override") << optfail('n') << defopt('y', "Override the project file") << query("Override"); Return_if_fail(prcsquery.result()); } version_log()->assign(option_version_log_string); } else { version_log()->assign(*new_version_log()); } new_version_log()->truncate(0); /* the Checkin-Login attribute */ checkin_login()->assign(get_login()); set_full_version (true); return NoError; } PrVoidError check_project(ProjectDescriptor *project) { const char* name; foreach_fileent(fe_ptr, project) { FileEntry *fe = *fe_ptr; name = fe->working_path(); if(!fe->on_command_line() && fe->empty_descriptor()) { /* if the user is doing a partial operation, the file needs * to have a non-empty internal descriptor, the descriptor * is currently, empty, and init has asked to register new * files for empty descriptors, complain. */ If_fail (fe->stat()) { pthrow prcserror << "The file " << squote(name) << " has an empty descriptor and does not exist. You may not " "do a partial checkin excluding files with empty descriptors" << dotendl; } else { static BangFlag bang; prcsquery << "The file " << squote(name) << " has an empty descriptor and was excluded on the command line. Partial checkins may not ignore files with empty descriptors. " << force("Continuing") << report("Continue") << allow_bang(bang) << optfail('n') << defopt('y', "Select this file, as if it were on the command line") << query("Select this file"); Return_if_fail(prcsquery.result()); fe->set_on_command_line(true); } } else if(fe->file_type() == RealFile && fe->descriptor_name()) { RcsVersionData* data; /* Check to see that the version exists, even if not on the * command line */ If_fail(data << project->repository_entry()-> lookup_rcs_file_data(fe->descriptor_name(), fe->descriptor_version_number())) { pthrow prcserror << "The missing file may have been in a deleted project version" << dotendl; } } } return NoError; } #ifndef NDEBUG static PrVoidError invalid() { fs_copy_filename(temp_file_1, bug_name()); prcserror << "Generated an invalid project file, aborting" << dotendl; return bug (); } #endif static PrVoidError write_new_prjfile(ProjectDescriptor *P, ProjectVersionData *parent) { #ifndef NDEBUG ProjectDescriptor* copy; Return_if_fail(P->write_project_file(temp_file_1)); If_fail(copy << read_project_file(P->project_full_name(), temp_file_1, false, KeepNothing)) { return invalid(); } foreach_fileent(fe_ptr, copy) { FileEntry* fe = *fe_ptr; if ( fe->empty_descriptor() ) { /* Hopefully this will catch Gene's problem. */ return invalid(); } } /*delete copy;*/ #endif Return_if_fail(P->write_project_file(cmd_root_project_file_path)); Return_if_fail(P->checkin_project_file(parent)); return NoError; } static void checkin_hook (void* v_fe, const char* new_version) { FileEntry *fe = (FileEntry*) v_fe; fe->set_version_number (new_version); } static PrVoidError checkin_each_file(ProjectDescriptor *project) { /* inconsistencies have been checked the Files attribute has been * modified already, excepting the new RCS version numbers */ foreach_fileent(fe_ptr, project) { FileEntry *fe = *fe_ptr; if(!fe->real_on_cmd_line() || fe->unmodified()) continue; Return_if_fail(VC_checkin(fe->temp_file_path(), fe->descriptor_version_number(), fe->full_descriptor_name(), NULL, fe, checkin_hook)); } Dstring rlog; rlog.sprintf (rlog_header, prcs_version_number[0], prcs_version_number[1], prcs_version_number[2]); Return_if_fail(VC_checkin(NULL, NULL, NULL, rlog, NULL, checkin_hook)); /* @@@ Really should do some verification here. It seems that the * above chmod can reach an NFS server before the new RCS versions * do... Dan Bonachea's problem. */ foreach_fileent(fe_ptr, project) { FileEntry *fe = *fe_ptr; if(!fe->real_on_cmd_line() || fe->unmodified()) continue; RcsVersionData ver; Return_if_fail (VC_get_one_version_data (fe->full_descriptor_name (), fe->descriptor_version_number (), & ver)); fe->set_lines (ver.plus_lines (), ver.minus_lines ()); /* RCS 5.7 does not preserve the mode of an empty version file * created by rcs -i, so we ignore the umask when registering * new files and just chmod the first version. */ if(strcmp(fe->descriptor_version_number(), "1.1") == 0) { Return_if_fail (project->repository_entry()-> Rep_chown_file(fe->descriptor_name())); } } return NoError; } /* * eliminate_unnamed_files -- * * this marks files that were not given on the command line so * that they can be ignored in certain stages of the checkin (and * other) commands. if no files were given, it leaves them all * marked, otherwise, it unmarks them all and marks all files (and * recursively marks directories). */ void eliminate_unnamed_files(ProjectDescriptor* project) { if(cmd_filenames_count < 1) { if (! option_exclude_project_file) cmd_prj_given_as_file = true; return; } for(int i = 0; i < cmd_filenames_count; i += 1) { /* special case, the project file */ if(strcmp(cmd_root_project_file_path, cmd_corrected_filenames_given[i]) == 0) { cmd_filenames_found[i] = true; cmd_prj_given_as_file = true; } } foreach_fileent(fe_ptr, project) { FileEntry *fe = *fe_ptr; fe->set_on_command_line(false); for(int j = 0; j < cmd_filenames_count; j += 1) { const char *f, *w; w = fe->working_path(); f = cmd_corrected_filenames_given[j]; while(*f && *w && *f == *w) { while(f[1] == '/') { f += 1; } while(w[1] == '/') { w += 1; } f += 1; w += 1; } if (*f == 0 && (w[-1] == '/' || *w == 0 || f[-1] == '/')) { cmd_filenames_found[j] = true; fe->set_on_command_line(true); } } } } /* * warn_unused_files -- * * this optionally prompts the user to continue and warns if any files * were not matched by a call to eliminate_unnamed_files. * * if prompt_abort is true, returns an error if the user does not * confirm ignoring unmatched filenames. * * if prompt_abort is false, it will return an error if files are unmatched. */ PrVoidError warn_unused_files(bool prompt_abort) { bool any = false, ret = true, need_prompt = false; if(cmd_filenames_count < 1) return NoError; for(int j = 0; j < cmd_filenames_count; j += 1) { any |= cmd_filenames_found[j]; if(!cmd_filenames_found[j]) { if(prompt_abort) { need_prompt = true; prcswarning << "File or directory " << squote(cmd_filenames_given[j]) << " on command line does not match any working files" << dotendl; } else { ret = false; prcswarning << "File or directory " << squote(cmd_filenames_given[j]) << " on command line did not match any working files" << dotendl; } } } if(!any) pthrow prcserror << "No files on command line match working files" << dotendl; if(need_prompt) { prcsquery << "Files on the command line did not match files in the project. " << force("Continuing") << report("Continue") << optfail('n') << defopt('y', "Continue, ignoring these files") << query("Continue"); Return_if_fail(prcsquery.result()); } if(ret) return NoError; else return PrVoidError(NonFatalError); } static PrVoidError maybe_query_user(ProjectDescriptor* P) { int tty, c, lc, llc; if(P->new_version_log()->length() > 0 || !get_environ_var("PRCS_LOGQUERY") || /* either it has been edited or PRCS_LOGQUERY is not set */ option_report_actions || option_force_resolution || option_version_log) return NoError; tty = isatty(STDIN_FILENO); if(tty) { prcsquery << "Empty New-Version-Log. " << option('e', "Enter version log now") << defopt('p', "Proceed with an empty version log") << query("Enter a log, or proceed"); char c; Return_if_fail(c << prcsquery.result()); if(c == 'p') return NoError; prcsoutput << "Enter description terminated by a single '.' or EOF" << dotendl; fprintf(stdout, ">> "); } re_query_message = ">> "; re_query_len = 3; lc = '\n'; llc = '\n'; while(true) { If_fail(c << Err_fgetc(stdin)) pthrow prcserror << "Read failure on stdin" << perror; if (c == EOF) { fprintf (stdout, "\n"); break; } if(tty && c == '\n') { if(lc == '.' && llc == '\n') { P->new_version_log()->truncate(P->new_version_log()->length() - 1); break; } fprintf(stdout, ">> "); } llc = lc; lc = c; P->new_version_log()->append(c); } if(tty) fprintf(stdout, "Done.\n"); /* I don't think its possible that PRCS will ever need to query the * user after this, but I might as well reopen it. */ if(freopen(ctermid(NULL), "r", stdin) == NULL) pthrow prcserror << "Couldn't reopen the standard input" << perror; return NoError; } PrProjectVersionDataPtrError ProjectDescriptor::resolve_checkin_version(const char *maj, ProjectVersionData **pred_project_data) { ProjectVersionData *cur_project_data = new ProjectVersionData(-1); ProjectVersionData *first_parent_project_data = NULL; Dstring *new_major = new Dstring; /* Most of this stuff leaks. */ Dstring *new_minor = new Dstring; int prev_highest_minor; /* Move New-Merge-Parents to Merge-Parents. */ Return_if_fail(adjust_merge_parents()); /* Find first parent version, if any. */ if(strcmp(*project_version_minor(), "0") != 0) { /* Don't beleive what's in the project file, make sure its in the * repository. */ /* @@@ I am concerned that all callers of lookup_version are * forced to check the same error conditions (NULL && * ->deleted()), but I don't know why it should be this way. */ first_parent_project_data = repository_entry()->lookup_version(this); if(!first_parent_project_data) { pthrow prcserror << "Illegal version " << full_version() << " in working project file" << dotendl; } if(first_parent_project_data->deleted ()) { pthrow prcserror << "Project version " << first_parent_project_data << " has been deleted" << dotendl; } cur_project_data->new_parent(first_parent_project_data->version_index()); if (_merge_parents->length() > 0) { /* See if the previous merge was complete. */ if (_merge_parents->last_index()->state & MergeStateIncomplete) { /* Last merge is incomplete, ask to abort this one and finish last one. */ Return_if_fail (merge_help_query_incomplete(_merge_parents->last_index())); } /* Add in any merge parents. */ foreach (ent_ptr, _merge_parents, MergeParentEntryPtrArray::ArrayIterator) { MergeParentEntry *ent = *ent_ptr; if (ent->state & MergeStateParent) { cur_project_data->new_parent(ent->project_data->version_index()); } } } } else { /* We assume that the first parent is a the Parent-Version field, so we can't * add Merge-Parents if we didn't add a Parent-Version. */ if (_merge_parents->length() > 0) { pthrow prcserror << "Kind of strange to have merge parents when you're an empty " "version, isn't it?" << prcsendl; } } /* Compute the new major version. */ if(!maj[0]) { new_major->assign(*project_version_major()); } else if(strcmp(maj, "@") == 0) { int highest_major; if(repository_entry()->version_count() == 0) pthrow prcserror << "No versions in the repository, can't resolve major version '@'" << dotendl; highest_major = repository_entry()->highest_major_version(); if(highest_major >= 0) { new_major->assign_int(highest_major); } else pthrow prcserror << "No numeric major versions in the repository, " "can't resolve major version '@'" << dotendl; } else { new_major->assign(maj); } /* Compute the new minor version. */ prev_highest_minor = repository_entry()->highest_minor_version(*new_major, true); new_minor->assign_int(prev_highest_minor + 1); cur_project_data->prcs_major (*new_major); cur_project_data->prcs_minor (*new_minor); /* Warn if the minor version specified on cmd line differs. */ if (cmd_version_specifier_minor && strcmp (cmd_version_specifier_minor, "@") != 0 && strcmp (cmd_version_specifier_minor, *new_minor) != 0) { prcsquery << "Minor version " << squote(cmd_version_specifier_minor) << " will be ignored, new minor is " << new_minor << ". " << force("Continuing") << report("Continue") << optfail('n') << defopt('y', "Continue and ignore supplied minor") << query("Continue"); Return_if_fail(prcsquery.result()); } if(!repository_entry()->major_version_exists(*new_major)) { /* If the branch is new, no need to check if the checkin is okay. */ if(repository_entry()->version_count() != 0) { /* Other majors do exist, confirm creation. */ prcsquery << "No previous major version named " << squote(new_major) << ". " << force("Creating") << report("Create") << optfail('n') << defopt('y', "Create the new branch and continue") << query("Create"); Return_if_fail(prcsquery.result()); } *pred_project_data = first_parent_project_data; } else { /* We've already got a ProjectVersionData entry for the working * version, its nameless. Now we check the common ancestor of * it and the previous highest minor version on the branch we're * checking into. If this common ancestor is equal to the * previous highest, then things are cool. If they are not, then * we warn the user and ask them what to do. */ ProjectVersionDataPtrArray *ancestors; ProjectVersionData *new_data; new_data = repository_entry()->lookup_version (*new_major, prev_highest_minor); ASSERT (new_data, "the minor number was already verified."); ancestors = repository_entry()-> common_version(cur_project_data, new_data); bool ancestor_unique = ancestors->length() == 1; bool ancestor_diff = !ancestor_unique || ancestors->index(0) != new_data; if (! ancestor_unique) { /* The common ancestor is troublesome. Query now. */ if (ancestors->length() == 0) { prcsquery << "No common ancestor between new version and working version. "; } else { prcsquery << "Ambiguous common ancestor between new version and working version. "; } } else if (ancestor_diff) { /* The common ancestor is unique, but there are versions * in the way, so query. */ prcsquery << "Intervening versions have been checked in between the target version, " << cur_project_data << ", and the working version's common ancestor, " << ancestors->index(0) << ". "; } if (!ancestor_unique || ancestor_diff) { prcsquery << report ("Query Abort/Force Continue") << force ("Continuing", 'n') << option ('n', "Continue, ignoring warning") << deffail ('y') << query ("Abort"); Return_if_fail (prcsquery.result()); } /* You're allowed to checkin past the deleted end of a branch. In that case its * as if the ancestry starts fresh with this checkin. */ if (!new_data->deleted()) { *pred_project_data = new_data; } delete ancestors; } return cur_project_data; } void checkin_prj_hook (void* v_new_rcs_version, const char* new_version) { ((Dstring*)v_new_rcs_version)->assign(new_version); } PrVoidError ProjectDescriptor::checkin_project_file(ProjectVersionData *new_project_data) { struct stat buf; Dstring version_log, new_rcs_version; foreach_fileent(fe_ptr, this) (*fe_ptr)->update_repository(repository_entry()); If_fail(Err_stat(project_file_path(), &buf)) pthrow prcserror << "Stat failed on newly written project file " << project_file_path() << perror; new_project_data->date(get_utc_time_t()); new_project_data->author(get_login()); new_project_data->length(buf.st_size); format_version_log(new_project_data, version_log); Dstring tmp_name (repository_entry()->Rep_name_of_version_file()); const char* abs_path; Return_if_fail (abs_path << absolute_path (project_file_path())); tmp_name.truncate (tmp_name.length() - 2); unlink (tmp_name); If_fail (Err_symlink (abs_path, tmp_name)) pthrow prcserror << "Error creating symbolic link " << squote(tmp_name) << perror; If_fail(VC_checkin(tmp_name, "", repository_entry()->Rep_name_of_version_file(), NULL, &new_rcs_version, checkin_prj_hook)) pthrow prcserror << "Failure checking in project file" << dotendl; If_fail(VC_checkin(NULL, NULL, NULL, version_log, NULL, checkin_prj_hook)) pthrow prcserror << "Failure checking in project file" << dotendl; unlink (tmp_name); // @@@ Really should do some verification here to work around NFS bugs if (strcmp (new_rcs_version, "1.1") == 0) { Dstring desc_name (project_name()); desc_name.append (".prj"); /* RCS 5.7 does not preserve the mode of an empty version file * created by rcs -i, so we ignore the umask when registering * new files and just chmod the first version. */ Return_if_fail(repository_entry()->Rep_chown_file(desc_name)); } new_project_data->rcs_version(new_rcs_version); /* Keith Owens 1.3.0 bug: if we crash right here then the RCS file * has been updated but not the prcs_data file. This causes * future checkins to get the wrong parent version index. This is * now checked during RepEntry::init. */ repository_entry()->add_project_data(new_project_data); Return_if_fail(repository_entry()->close_repository()); return NoError; } void format_version_log(ProjectVersionData* project_data, Dstring& log) { static const char prcs_version_format[] = "PRCS major version: %s\n" "PRCS minor version: %s\n"; log.sprintf(rlog_header, prcs_version_number[0], prcs_version_number[1], prcs_version_number[2]); log.sprintfa(prcs_version_format, project_data->prcs_major(), project_data->prcs_minor()); if (project_data->parent_count() > 0) { log.sprintfa("PRCS parent indices: %d", project_data->parent_index(0)); if (project_data->parent_count() > 1) { for (int i = 1; i < project_data->parent_count(); i += 1) log.sprintfa(":%d", project_data->parent_index(i)); } log.sprintfa("\n"); } if(project_data->deleted()) log.append("PRCS version deleted\n"); } void ProjectDescriptor::quick_elim_unmodified() { foreach_fileent(fe_ptr, this) { FileEntry* fe = *fe_ptr; if(!fe->real_on_cmd_line() || !fe->descriptor_name()) continue; if(_quick_elim && _quick_elim->unchanged(fe)) { DEBUG("Quick elim succeeds for file " << squote(fe->working_path())); fe->set_unmodified(true); } } } static PrVoidError slow_eliminate_prepare(ProjectDescriptor *project) { static MemorySegment file_buffer(false); static MemorySegment setkey_buffer(false); foreach_fileent(fe_ptr, project) { FileEntry *fe = *fe_ptr; const char *output_buffer; int output_len; bool setkeys; char* checksum; if(!fe->real_on_cmd_line() || fe->unmodified()) continue; Return_if_fail(file_buffer.map_file(fe->working_path())); if(fe->keyword_sub()) { If_fail( setkeys << setkeys_inoutbuf(file_buffer.segment(), file_buffer.length(), &setkey_buffer, fe, Unsetkeys)) pthrow prcserror << "Keyword replacement failed on file " << squote(fe->working_path()) << perror; output_buffer = setkey_buffer.segment(); output_len = setkey_buffer.length(); } else { output_buffer = file_buffer.segment(); output_len = file_buffer.length(); } checksum = md5_buffer(output_buffer, output_len); fe->set_checksum(checksum); fe->set_unkeyed_length(output_len); if(fe->descriptor_name()) { /* If it doesn't have an empty descriptor. */ RcsVersionData *old_data; Return_if_fail(old_data << project->repository_entry() -> lookup_rcs_file_data(fe->descriptor_name(), fe->descriptor_version_number())); if (output_len == old_data->length() && memcmp(checksum, old_data->unkeyed_checksum(), 16) == 0) { DEBUG("Slow elim succeeds for file " << squote(fe->working_path())); fe->set_unmodified (true); continue; } } Return_if_fail(fe->initialize_descriptor(project->repository_entry(), !option_report_actions, true)); /* Can't free the rep_comp_cache for these initializations, because * of the batch nature of this checkin. If you ever change that, be * sure to clean up. */ if(!option_report_actions) { unlink(fe->temp_file_path()); const char* fullpath; if(fe->keyword_sub() && setkeys) { FILE* temp_out; fullpath = make_temp_file(""); If_fail(temp_out << Err_fopen(fullpath, "w")) pthrow prcserror << "Failed opening temp file for writing " << squote(fullpath) << perror; If_fail (Err_fwrite(output_buffer, output_len, temp_out)) pthrow prcserror << "Failed writing temp file " << squote(fullpath) << perror; If_fail (Err_fclose(temp_out)) pthrow prcserror << "Failed writing temp file " << squote(fullpath) << perror; } else { Return_if_fail(fullpath << name_in_cwd(fe->working_path())); } If_fail (Err_symlink(fullpath, fe->temp_file_path())) { pthrow prcserror << "Error creating symbolic link " << squote(fe->temp_file_path()) << perror; } #if defined (PRCS_DEVEL) && ! defined(NDEBUG) if (option_debug) { struct stat sbuf; /* The link broke once on scheme.xcf, while running as * root with -j32 on a project consisting of the gcc-2.7.2.2 * source tree with all files zeroed. */ If_fail (Err_stat (fe->temp_file_path(), &sbuf)) { prcserror << "Newly created symlink " << squote(fe->temp_file_path()) << " doesn't stat" << dotendl; abort(); } } #endif } } if(!option_report_actions) Return_if_fail(VC_register(NULL)); return NoError; } void FileEntry::update_repository(RepEntry* rep_entry) const { if(real_on_cmd_line() && !unmodified()) { RcsVersionData ver; ver.date (get_utc_time_t()); ver.author (get_login()); ver.length (unkeyed_length()); ver.unkeyed_checksum (checksum()); ver.rcs_version (descriptor_version_number()); ver.set_plus_lines (plus_lines ()); ver.set_minus_lines (minus_lines ()); rep_entry->add_rcs_file_data(descriptor_name(), &ver); } } static PrVoidError checkin_cleanup_handler(void* data, bool sig) { if (!sig) return NoError; ProjectDescriptor* project = (ProjectDescriptor* ) data; foreach_fileent(fe_ptr, project) { FileEntry* fe = *fe_ptr; if(!fe->on_command_line()) continue; if(fe->temp_file_path() != NULL) unlink(fe->temp_file_path()); } return NoError; } /* Given an input string, it modiifes the string to contain the parent path. * Returns false if there are no more entries. Path may not be empty. */ static bool parent_path_of(Dstring &string) { int inlen = string.length(); ASSERT(inlen > 0, ""); /* First remove any trailing slash, then remove the trailing component. */ if (string.index(inlen-1) == '/') { string.truncate(inlen-1); } /* Earlier steps avoid the case of a "/" path name, so the path should * still be non-empty. */ ASSERT(string.length() > 0, ""); /* The length of the A/B/C/D/ compent, minus the length of E. */ const char *p = string.cast(); int len = strip_leading_path(p) - p; if (len == 0) { return false; } /* Leave a trailing '/'. This is important for the strncmp() in * maybe_void_file_entry_subdir. */ string.truncate(len); ASSERT (string.index(string.length() - 1) == '/', ""); return true; } /* Returns whether to skip this file entry because, for example, we already * determined that its parent directory doesn't exist (and has been ignored). * It is given that stat() or lstat() failed on this file, and this performs * one of several possible actions: query the user, report to the user, issue * a report for forced operations, etc. */ static PrBoolError maybe_void_file_entry_subdir(FileEntry* fe, MissingFileAction action) { static DstringPtrArray* eliminated_subdirs = NULL; struct stat buf; const char* name = fe->working_path(); bool has_parent; if(!eliminated_subdirs) eliminated_subdirs = new DstringPtrArray; /* If it doesn't have any slashes in the path, then there are no parents * for this entry to be a subdirectory of. */ if(!strchr(name, '/')) return false; /* The two path variables. The first value for parent_path is the parent * of the argument. Then we walk up the path using this_path and * parent_path. */ Dstring parent_path (name); Dstring this_path; /* Now reduce parent_path to the last trailing slash. */ has_parent = parent_path_of(parent_path); ASSERT (has_parent, "checked in strchr() above"); /* Warning: this is linear search in a nested loop. */ foreach(elim_ptr, eliminated_subdirs, DstringPtrArray::ArrayIterator) { Dstring* elim = *elim_ptr; if(strncmp(elim->cast(), name, elim->length()) == 0) { switch (action) { case QueryUserRemoveFromCommandLine: fe->set_on_command_line(false); break; case NoQueryUserRemoveFromCommandLine: fe->set_on_command_line(false); break; case SetNotPresentWarnUser: fe->set_present(false); break; } return true; } } if(stat(parent_path, &buf) >= 0 || errno != ENOENT) return false; for (;;) { this_path.assign(parent_path); has_parent = parent_path_of(parent_path); if (! has_parent) { break; } const char* s = parent_path; if(stat(s[0] == 0 ? "." : s, &buf) >= 0) { static BangFlag bang; switch (action) { case QueryUserRemoveFromCommandLine: prcsquery << "The directory " << squote(this_path) << " does not exist. " << force("Ignoring all entries") << report("Ignore all entries") << optfail('n') << defopt('y', "Ignore all entries of this directory") << allow_bang (bang) << query("Ignore it and descendants"); Return_if_fail(prcsquery.result()); fe->set_on_command_line(false); break; case NoQueryUserRemoveFromCommandLine: prcswarning << "The directory " << squote(this_path) << " does not exist. Ignoring all entries" << dotendl; fe->set_on_command_line(false); break; case SetNotPresentWarnUser: prcswarning << "The directory " << squote(this_path) << " does not exist. Assuming all entries are unmodified" << dotendl; fe->set_present(false); break; } eliminated_subdirs->append(new Dstring(this_path)); return true; } else if(errno != ENOENT) { return false; } } return false; } PrVoidError eliminate_working_files(ProjectDescriptor* project, MissingFileAction action) { foreach_fileent(fe_ptr, project) { FileEntry* fe = *fe_ptr; if(fe->on_command_line()) { static BangFlag bang; bool file_present; Return_if_fail(file_present << fe->check_working_file()); if(file_present) continue; bool ignore_subdir; Return_if_fail(ignore_subdir << maybe_void_file_entry_subdir(fe, action)); if(ignore_subdir) continue; switch (action) { case QueryUserRemoveFromCommandLine: prcsquery << "File " << squote(fe->working_path()) << " is unavailable. " << force("Continuing") << report("Continue") << allow_bang(bang) << defopt('y', "Ignore this file, as if it were not on the command line") << optfail('n') << query("Ignore this file"); Return_if_fail(prcsquery.result()); fe->set_on_command_line(false); break; case NoQueryUserRemoveFromCommandLine: prcswarning << "File " << squote(fe->working_path()) << " is unavailable. Continuing" << dotendl; fe->set_on_command_line(false); break; case SetNotPresentWarnUser: prcswarning << "File " << squote(fe->working_path()) << " is unavailable. Assuming unmodified" << dotendl; fe->set_present(false); break; } } } return NoError; }