//
// aegis - project change supervisor
// Copyright (C) 2004-2009, 2011-2014 Peter Miller
// Copyright (C) 2008 Walter Franzini
//
// 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 3 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, see .
//
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
module_change::~module_change()
{
cp.reset();
pp->free();
pp = 0;
}
module_change::module_change(const change::pointer &arg) :
cp(arg),
pp(project_copy(arg->project_get())),
up(user_ty::create())
{
}
void
module_change::modified(server_ty *sp, string_ty *file_name, file_info_ty *fip,
const input::pointer &contents)
{
//
// It is an error if the change is not in the "being developed" state.
//
if (!cp->is_being_developed())
{
server_error
(
sp,
"Modified: project \"%s\": change %ld: this change must be "
"in the \"being_developed\" state for this to work",
project_name_get(pp).c_str(),
cp->number_get()
);
return;
}
//
// It is an error if the change is actually a branch.
//
if (cp->was_a_branch())
{
server_error
(
sp,
"Modified: project \"%s\": change %ld: is a branch; to modify "
"files you must use a change on the branch",
project_name_get(pp).c_str(),
cp->number_get()
);
return;
}
//
// It is an error if the change is not assigned to the current user.
//
if (nstring(cp->developer_name()) != up->name())
{
server_error
(
sp,
"Modified: project \"%s\": change %ld: is owned by user \"%s\", "
"but it must be owned by user \"%s\" for this to work",
project_name_get(pp).c_str(),
cp->number_get(),
cp->developer_name()->str_text,
up->name().c_str()
);
}
//
// If the file doesn't exist in the change, copy it or create it
// as appropriate.
//
fstate_src_ty *csrc = cp->file_find(nstring(file_name), view_path_first);
if (!csrc)
{
string_ty *qf;
const char *verb;
nstring the_command;
int ok;
nstring qp(project_name_get(pp).quote_shell());
qf = str_quote_shell(file_name);
if (!pp->file_find(file_name, view_path_extreme))
{
//
// FIXME: what if it's a new test?
// (aepatch has the same problem)
//
verb = "-new-file";
}
else
{
verb = "-copy-file";
}
the_command =
nstring::format
(
"aegis %s -project=%s -change=%ld -base-relative -v %s",
verb,
qp.c_str(),
cp->number_get(),
qf->str_text
);
str_free(qf);
//
// Run the command.
// if there is a problem, it will also send the error to the client.
//
ok = server_execute(sp, the_command.get_ref());
if (!ok)
return;
//
// Change objects can be very long lived in the aecvsserver,
// so make sure that we re-read the meta data soon.
//
cp->lock_sync_forced();
//
// Now re-get the file information.
//
csrc = cp->file_find(nstring(file_name), view_path_first);
assert(csrc);
}
else if (csrc->action == file_action_remove)
{
server_error
(
sp,
"Modified: project \"%s\": change %ld: file \"%s\" is being "
"removed, it makes no sense to say it's been modified",
project_name_get(pp).c_str(),
cp->number_get(),
file_name->str_text
);
return;
}
//
// Figure the path of the file to be written.
//
string_ty *dd = cp->development_directory_get(0);
string_ty *abs_file_name = os_path_cat(dd, file_name);
//
// Normalize the mode (Aegis has its own idea about file modes).
//
if (fip->mode & 0222)
fip->mode |= 0222;
if (fip->mode & 0111)
fip->mode |= 0111;
fip->mode |= 0644;
fip->mode &= ~cp->umask_get();
//
// Copy the file contents to their destination.
//
os_become_orig();
output::pointer op = output_file::binary_open(abs_file_name);
op << contents;
op.reset();
//
// And make sure it is in the specified mode.
//
os_chmod(abs_file_name, fip->mode);
os_become_undo();
str_free(abs_file_name);
}
string_ty *
module_change::calculate_canonical_name()
const
{
// FIXME: memory leak
return
str_format
(
"%s.C%3.3ld",
project_name_get(pp).c_str(),
cp->number_get()
);
}
bool
module_change::update(server_ty *sp, string_ty *, string_ty *server_side_0,
const options &opt)
{
size_t j;
static string_ty *minus_str;
static string_ty *zero;
//
// It is an error if the change is not in the "being developed" state.
//
// FIXME: actually, it's OK if the file is between being_developed
// and being_integrated, but only read-only for the later ones.
//
// FIXME: what about changes in the completed state, we will need
// to use project_file_roll_forward instead.
//
if (!cp->is_being_developed())
{
server_error
(
sp,
"project \"%s\": change %ld: this change must be in the "
"\"being_developed\" state for this to work",
project_name_get(pp).c_str(),
cp->number_get()
);
return false;
}
//
// Form a list of files by unioning the source files of the change
// and all the ancestor branches together.
//
// For each of these files (that the client wants to know about)
// send information about their contents.
//
for (j = 0; ; ++j)
{
fstate_src_ty *src;
string_ty *client_side;
string_ty *server_side;
string_ty *path;
int mode;
string_ty *version;
int is_local;
file_info_ty *fip;
src = change_file_nth(cp, j, view_path_simple);
if (!src)
break;
switch (src->usage)
{
case file_usage_build:
continue;
case file_usage_source:
case file_usage_config:
case file_usage_test:
case file_usage_manual_test:
break;
}
//
// Make sure the client creates the directories for us.
//
server_side = os_path_cat(name(), src->file_name);
if (!is_update_prefix(server_side_0, server_side, opt.d))
{
//
// don't create files which are not under one of the
// Directories specified by the client.
//
str_free(server_side);
continue;
}
client_side = server_directory_calc_client_side(sp, server_side);
server_mkdir_above(sp, client_side, server_side);
server_updating_verbose(sp, client_side);
//
// Determine where to get the file from.
//
version = 0;
bool need_to_unlink = false;
path = cp->file_version_path(src, need_to_unlink);
is_local = !!cp->file_find(nstring(src->file_name), view_path_first);
os_become_orig();
input::pointer ip = input_file::open(nstring(path), need_to_unlink);
if (is_local && os_executable(path))
src->executable = true;
if (is_local)
version = fake_version(os_mtime_actual(path));
os_become_undo();
str_free(path);
//
// Determine the file mode.
//
mode = 0666;
if (src->executable)
mode |= 0111;
mode &= ~cp->umask_get();
//
// Determine the version string to send to the client.
// Special cases:
// "" - no user file,
// "0" - new user file,
// "-" - user file to be removed
//
if (!version)
{
switch (src->action)
{
case file_action_remove:
//
// What do do depends on whether the file exists on the
// client or not.
//
// the client has never heard of it
// do nothing
//
// The client has no Entry for it, but said Questionable
// do nothing
//
// the client Entry has a removed version ("-")
// do nothing.
//
// the client Entry has a new file version ("0")
// do nothing.
//
// the client says it exists
// if Is-modified
// send the new file ("0") Entry
// otherwise
// send the Removed response, which will remove
// the entry and the file from the client side.
//
fip = server_file_info_find(sp, server_side, 0);
if (!fip || !fip->version)
goto do_nothing;
if (!minus_str)
minus_str = str_from_c("-");
if (!zero)
zero = str_from_c("0");
if
(
str_equal(fip->version, minus_str)
||
str_equal(fip->version, zero)
)
goto do_nothing;
if (fip->modified > 0)
{
version = str_copy(zero);
break;
}
server_response_queue
(
sp,
new response_removed(client_side, server_side)
);
goto do_nothing;
case file_action_transparent:
version = str_from_c("0");
break;
case file_action_create:
case file_action_modify:
case file_action_insulate:
if (src->edit && src->edit->revision)
version = str_copy(src->edit->revision);
else
version = str_from_c("0");
break;
}
if (!version)
version = str_from_c("");
}
fip = server_file_info_find(sp, server_side, 0);
if (!fip)
{
//
// Queue the response to be sent.
//
server_response_queue
(
sp,
new response_created
(
client_side,
server_side,
ip,
mode,
version
)
);
}
else if (opt.C)
{
//
// We have been told to over-write what they have on the
// client side.
//
if (fip->modified > 0)
{
server_response_queue
(
sp,
new response_update_existing
(
client_side,
server_side,
ip,
mode,
version
)
);
}
}
else if (str_equal(fip->version, version))
{
//
// What they copied the first time is still the current
// version on the server side.
//
if (fip->modified > 0)
{
string_ty *sub;
//
// Remind them they need to commit it.
//
sub = os_entryname(client_side);
server_m(sp, "M %s\n", sub->str_text);
str_free(sub);
}
}
else
{
//
// What they copied the first time is no longer the
// current version on the server side.
//
if (fip->modified)
{
//
// The server side change and the client side changed.
// The Modified request (received earlier) has copied
// the file into the change, and the over-written it.
//
// We have to send the file back to them,
// AFTER running a merge command.
//
// FIXME: run the merge command
//
server_response_queue
(
sp,
new response_update_existing
(
client_side,
server_side,
ip,
mode,
version
)
);
}
else
{
//
// The client side is out-of-date, resend the file and
// over-write the client-side contents.
//
server_response_queue
(
sp,
new response_update_existing
(
client_side,
server_side,
ip,
mode,
version
)
);
}
}
do_nothing:
str_free(client_side);
str_free(server_side);
str_free(version);
os_become_orig();
ip.reset();
os_become_undo();
}
return true;
}
static bool
file_being_deleted(server_ty *sp, string_ty *server_side)
{
file_info_ty *fip;
static string_ty *minus_str;
fip = server_file_info_find(sp, server_side, 0);
if (!fip)
return false;
if (!fip->version)
return false;
if (!minus_str)
minus_str = str_from_c("-");
return str_equal(fip->version, minus_str);
}
bool
module_change::checkin(server_ty *sp, string_ty *client_side,
string_ty *server_side)
{
fstate_src_ty *src;
int mode;
string_ty *version;
string_ty *filename;
const char *strp;
//
// It is an error if the change is not in the "being developed" state.
//
if (!cp->is_being_developed())
{
server_error
(
sp,
"ci: project \"%s\": change %ld: this change must be "
"in the \"being_developed\" state for this to work",
project_name_get(pp).c_str(),
cp->number_get()
);
return false;
}
//
// Extract the baseline-relative name of the file.
//
// The server-side string sent by the client will be
// ROOT_PATH/project.Cnnn/filename so skip three slashes (ROOT_PATH
// starts with a slash) and use the rest. Except that the Directory
// request code strips off the ROOT_PATH/ part, so skip one slash
// instead.
//
strp = strchr(server_side->str_text, '/');
assert(strp);
filename = str_from_c(strp ? strp + 1 : ".");
src = cp->file_find(nstring(filename), view_path_first);
if (!src)
{
if (file_being_deleted(sp, server_side))
{
string_ty *qf;
string_ty *the_command;
int ok;
nstring qp(project_name_get(pp).quote_shell());
qf = str_quote_shell(filename);
the_command =
str_format
(
"aegis --remove-file --project=%s --change=%ld --base-relative -v %s",
qp.c_str(),
cp->number_get(),
qf->str_text
);
str_free(qf);
//
// Run the command. If there is a problem, it will also
// send the error to the client.
//
ok = server_execute(sp, the_command);
str_free(the_command);
if (!ok)
return false;
//
// Change objects can be very long lived in the aecvsserver,
// so make sure that we re-read the meta data soon.
//
cp->lock_sync_forced();
//
// Let the client know the file is well and truly gone.
// (We dont use the Removed response because the file is
// already deleted from thwe client.)
//
server_response_queue
(
sp,
new response_remove_entry(client_side, server_side)
);
//
// Report success.
//
return true;
}
server_error
(
sp,
"ci: project \"%s\": change %ld: file \"%s\" unknown",
project_name_get(pp).c_str(),
cp->number_get(),
filename->str_text
);
return false;
}
//
// For changes which are being developeed, like this one, we
// have to look at the file itself for the executable bit.
//
if (src->action != file_action_remove)
{
string_ty *path;
path = cp->file_path(filename);
os_become_orig();
if (os_executable(path))
src->executable = true;
os_become_undo();
str_free(path);
}
//
// Determine the file mode.
//
mode = 0666;
if (src->executable)
mode |= 0111;
mode &= ~cp->umask_get();
//
// Determine the version string to send to the client.
// Special cases:
// "" - no user file,
// "0" - new user file,
// "-" - user file to be removed
//
version = 0;
switch (src->action)
{
case file_action_remove:
version = str_from_c("-");
break;
case file_action_transparent:
version = str_from_c("0");
break;
case file_action_create:
case file_action_modify:
case file_action_insulate:
if (src->edit && src->edit->revision)
version = str_copy(src->edit->revision);
else
version = str_from_c("0");
break;
}
if (!version)
version = str_from_c("");
//
// Queue the response to be sent.
//
server_response_queue
(
sp,
new response_checked_in(client_side, server_side, mode, version)
);
str_free(version);
return true;
}
bool
module_change::add(server_ty *sp, string_ty *client_side,
string_ty *server_side, const options &)
{
fstate_src_ty *src;
int mode;
string_ty *version;
string_ty *filename;
const char *strp;
//
// It is an error if the change is not in the "being developed" state.
//
if (!cp->is_being_developed())
{
server_error
(
sp,
"add: project \"%s\": change %ld: this change must be "
"in the \"being_developed\" state for this to work",
project_name_get(pp).c_str(),
cp->number_get()
);
return false;
}
//
// Extract the baseline-relative name of the file.
//
// The server-side string sent by the client will be
// ROOT_PATH/project.Cnnn/filename so skip three slashes and use
// the rest. Except that the Directory request code strips off
// the ROOT_PATH/ part, so skip one slash instead.
//
strp = strchr(server_side->str_text, '/');
assert(strp);
filename = str_from_c(strp ? strp + 1 : ".");
//
// The client sends an "Is-modified" request, so the file isn't
// necesarily created yet.
//
src = cp->file_find(nstring(filename), view_path_first);
if (src)
{
//
// The CVS documentation says that a "cvs add" of a removed file
// will re-instate it.
//
if (src->action == file_action_remove)
{
string_ty *qf;
string_ty *the_command;
int ok;
nstring qp(project_name_get(pp).quote_shell());
qf = str_quote_shell(filename);
the_command =
str_format
(
"aegis -rmu -project=%s -change=%ld -base-relative -v %s",
qp.c_str(),
cp->number_get(),
qf->str_text
);
//
// Run the command.
// if there is a problem, it will also send the error to the client.
//
ok = server_execute(sp, the_command);
str_free(the_command);
if (!ok)
{
str_free(qf);
return false;
}
//
// Change objects can be very long lived in the aecvsserver,
// so make sure that we re-read the meta data soon.
//
cp->lock_sync_forced();
//
// Now re-get the file information.
//
src = pp->file_find(filename, view_path_extreme);
if (src)
{
the_command =
str_format
(
"aegis -copy-file -project=%s -change=%ld -base-relative -v %s",
qp.c_str(),
cp->number_get(),
qf->str_text
);
//
// Run the command. if there is a problem, it will also
// send the error to the client.
//
ok = server_execute(sp, the_command);
str_free(the_command);
if (!ok)
{
str_free(qf);
return false;
}
//
// Change objects can be very long lived in the aecvsserver,
// so make sure that we re-read the meta data soon.
//
cp->lock_sync_forced();
//
// Now re-get the file information.
//
src = cp->file_find(nstring(filename), view_path_first);
assert(src);
}
str_free(qf);
}
}
//
// For changes which are being developeed, like this one, we
// have to look at the file itself for the executable bit.
//
if (src && src->action != file_action_remove)
{
string_ty *path;
path = cp->file_path(filename);
os_become_orig();
if (os_executable(path))
src->executable = true;
os_become_undo();
str_free(path);
}
//
// Determine the file mode.
//
mode = 0666;
if (src && src->executable)
mode |= 0111;
mode &= ~cp->umask_get();
if (!src)
{
version = str_from_c("0");
server_e
(
sp,
"scheduling file `%s' for addition",
client_side->str_text
);
server_response_queue
(
sp,
new response_new_entry(client_side, server_side, mode, version)
);
str_free(version);
return true;
}
//
// Determine the version string to send to the client.
// Special cases:
// "" - no user file,
// "0" - new user file,
// "-" - user file to be removed
//
version = 0;
switch (src->action)
{
case file_action_remove:
version = str_from_c("-");
break;
case file_action_transparent:
case file_action_create:
version = str_from_c("0");
break;
case file_action_modify:
case file_action_insulate:
if (src->edit && src->edit->revision)
version = str_copy(src->edit->revision);
else
version = str_from_c("0");
break;
}
if (!version)
version = str_from_c("");
//
// Queue the response to be sent.
//
server_e
(
sp,
"scheduling file `%s' for addition",
src->file_name->str_text
);
server_response_queue
(
sp,
new response_new_entry(client_side, server_side, mode, version)
);
str_free(version);
return true;
}
bool
module_change::remove(server_ty *sp, string_ty *client_side,
string_ty *server_side, const options &)
{
fstate_src_ty *src;
int mode;
string_ty *version;
string_ty *filename;
const char *strp;
//
// It is an error if the change is not in the "being developed" state.
//
if (!cp->is_being_developed())
{
server_error
(
sp,
"remove: project \"%s\": change %ld: this change must be "
"in the \"being_developed\" state for this to work",
project_name_get(pp).c_str(),
cp->number_get()
);
return false;
}
//
// Extract the baseline-relative name of the file.
//
// The server-side string sent by the client will be
// ROOT_PATH/project.Cnnn/filename so skip three slashes and use
// the rest. Except that the Directory request code strips off
// the ROOT_PATH/ part, so skip one slash instead.
//
strp = strchr(server_side->str_text, '/');
assert(strp);
filename = str_from_c(strp ? strp + 1 : ".");
//
// If the file already exists in the change,
// we may have to undo that.
//
src = cp->file_find(nstring(filename), view_path_first);
if (src)
{
string_ty *qf;
string_ty *the_command;
int ok;
const char *verb;
verb = "--copy-file-undo";
switch (src->action)
{
case file_action_create:
verb = "--new-file-undo";
switch (src->usage)
{
case file_usage_source:
case file_usage_config:
case file_usage_build:
break;
case file_usage_test:
case file_usage_manual_test:
verb = "--new-test-undo";
break;
}
break;
case file_action_modify:
case file_action_insulate:
break;
case file_action_transparent:
verb = "--make-transparent-undo";
break;
case file_action_remove:
goto already_being_removed;
}
nstring qp = project_name_get(pp).quote_shell();
qf = str_quote_shell(filename);
the_command =
str_format
(
"aegis %s -project=%s -change=%ld -base-relative -v %s",
verb,
qp.c_str(),
cp->number_get(),
qf->str_text
);
str_free(qf);
//
// Run the command.
// if there is a problem, it will also send the error to the client.
//
ok = server_execute(sp, the_command);
str_free(the_command);
if (!ok)
return false;
//
// Change objects can be very long lived in the aecvsserver,
// so make sure that we re-read the meta data soon.
//
cp->lock_sync_forced();
switch (src->action)
{
case file_action_create:
server_response_queue
(
sp,
new response_remove_entry(client_side, server_side)
);
return true;
case file_action_remove:
case file_action_modify:
case file_action_insulate:
case file_action_transparent:
break;
}
src = 0;
}
src = pp->file_find(filename, view_path_extreme);
if (!src)
{
server_error
(
sp,
"project \"%s\": change %ld: file \"%s\" cannot be removed "
"because it does not exist in the project",
project_name_get(pp).c_str(),
cp->number_get(),
filename->str_text
);
return false;
}
//
// Determine the version string to send to the client.
// Special cases:
// "" - no user file,
// "0" - new user file,
// "-" - user file to be removed
//
already_being_removed:
version = str_from_c("-");;
mode = 0644;
//
// Queue the response to be sent.
//
server_e
(
sp,
"scheduling file `%s' for removal",
src->file_name->str_text
);
server_response_queue
(
sp,
new response_new_entry(client_side, server_side, mode, version)
);
str_free(version);
return true;
}
module_ty *
module_change_new(string_ty *project_name, long change_number)
{
//
// Make sure the project makes sense.
//
project *pp = project_alloc(project_name);
if (!pp->bind_existing_errok())
{
pp->free();
return new module_project_bogus(project_name);
}
//
// Make sure the change makes sense.
//
change::pointer cp = change::create(pp, change_number);
if (!cp->bind_existing_errok())
{
pp->free();
return new module_change_bogus(project_name, change_number);
}
//
// OK, looks like we have a viable change.
//
return new module_change(cp);
}
// vim: set ts=8 sw=4 et :