понедельник, 5 мая 2008 г.

Скрипт для помощи в разрешении конфликтов CVS

Время от времени, когда вы заливаете в CVS только что отредактированный файл, она выдает вам следующее сообщение:
cvs commit: Up-to-date check failed for `main.c'
cvs [commit aborted]: correct above errors first!
которое означает, что кто-то уже успел отредактировать этот файл и залить его в CVS раньше вас.

Встает проблема разрешения конфликта. Что делать? Выполнить cvs update и доверить разрешение конфликта CVS? Нет, я кроме себя никому не доверяю. :)

Лично мне в таких случаях всегда хочется видеть перед собой 3 файла - мой, который я только что отредактировал, файл с ревизией BASE и файл с ревизией HEAD, и уже самому, а не в автоматическом режиме, объединить две версии.

Небольшая справка, если вам не знакомы понятия BASE и HEAD ревизии:
BASE - это номер ревизии, которая была скачана вами из CVS, и которую вы впоследствии изменили.
HEAD - это номер самой последней ревизии, находящейся в CVS.


Почему 3 файла, а не только мой и HEAD? Так гораздо нагляднее. Сразу видно, кто какие изменения вносил в код, и гораздо проще понять, как все это объединить так, чтобы не нарушить логику работы программы. К примеру если оба человека одинаковым образом объявят новую переменную в начале функции, то при сравнении этих двух файлов строки, в которых объявлена переменная, будут одинаковыми, и diff их не подсветит. При сравнении же 3 файлов вы сразу заметите, что в обоих файлах появилась новая переменная с одинаковым именем, а значит это сразу будет сигналом для вас, что необходимо тщательно отследить, где она изменяется обоими авторами, чтобы их изменения переменной не пересекались, или вовсе переименовать в одной реализации эту переменную, чтобы явно исключить конфликт.

Исходя из вышеизложенных соображений, я написал скрипт для автоматизации этого процесса. Возможно он пригодится не только мне, поэтому выкладываю его сюда и опишу, как он работает.

Предположим, что вы только что выполнили команду
cvs commit
и она выдала вам предупреждение
cvs commit: Up-to-date check failed for `main.c'
cvs [commit aborted]: correct above errors first!

Вам нужно запустить мой скрипт, передав ему путь к файлу, конфликтующему с CVS:
cvs_resolve_conflict.sh main.c

Предполагается, что в процессе работы скрипта вы не будете работать с рабочей копией CVS, в которой находится данный файл, т. к. во время своей работы скрипт изменяет файл CVS/Entries из той директории, в которой лежит ваш файл.

После запуска скрипта он копирует ваш файл и все необходимые файлы CVS во временную директорию, чтобы лишний раз не повредить их, и скачивает ревизии BASE и HEAD. Как только все необходимые файлы получены, скрипт запускает программу Meld так, чтобы в левой колонке была ревизия HEAD, в правой - ваша версия, а в центре - ревизия BASE.

С помощью Meld объединять версии очень удобно (спасибо разработчикам!). При сливании перемещайте куски кода из своей версии и ревизии HEAD в ревизию BASE (с краев в центр).

Hint: При объединении кусков кода с помощью Meld очень удобно использовать стрелки. При нажатии на Ctrl количество стрелок увеличивается, что позволяет вставлять код вместо, перед и после существующего кода. Shift тоже имеет специальное значение - с его помощью можно отменять изменения в какой-либо версии.

Если после закрытия Meld скрипт обнаружит, что файл, который был открыт в средней колонке, изменился, он спросит, стоит ли сохранить изменения. Если вы откажетесь от сохранения изменений, то вернетесь к тому состоянию, которое было до запуска скрипта. Если же решите сохранить изменения, то скрипт зайдет в вашу рабочую копию CVS, заменит CVS/Entries новым, в котором прописана версия, конфликт с которой вы уже разрешили, и заменит старый файл тем, который вы только что сформировали в средней колонке при помощи Meld. Теперь вам останется только сделать cvs commit.

Также хочу обратить внимание на то, что если во время работы скрипта кто-то опять зальет новую версию вашего файла, т. е. изменится номер HEAD ревизии, то скрипт сработает нормально. А именно: в таком случае вы разрешите конфликт только со "старым HEAD", и после завершения скрипта при выполнении cvs commit СVS выдаст вам ошибку конфликта "старого HEAD с новым HEAD" которую вы опять сможете разрешить с помощью моего скрипта.

В скрипте установлен обработчик сигналов SIGHUP, SIGINT, SIGQUIT и SIGTERM. Поэтому во время работы скрипта можно его прерывать, например, комбинацией Ctrl+C.

На представленных ниже скриншотах я привел простейший пример разрешения конфликта, чтобы вы смогли оценить удобство использования Meld.

Удачи!




#!/bin/bash
#***************************************************************************
#*   Copyright (C) 2008, Konishchev Dmitry                                 *
#*   http://konishchevdmitry.blogspot.com/2008/05/cvs.html                 *
#*                                                                         *
#*   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.                          *
#**************************************************************************/


# Функции -->
cleanup()
{
local tmp_path="/tmp/resolve_cvs_file"

if [[ -e "$temp_dir" ]]
then
# Проверяем на всякий случай то, что мы удаляем.
# Использование rm -r очень опасно!
# Если в скрипте присутствует ошибка при присвоении
# значения переменной temp_dir, то последствия могут
# быть весьма печальными.
if [[ ${temp_dir:0:${#tmp_path}} == $tmp_path ]]
then
rm -r "$temp_dir"
else
echo "Logical error!" >&2
fi
fi

if [[ -e "$resolve_file_dir/$resolve_file_name".bak ]]
then
if [[ -e "$resolve_file_dir/$resolve_file_name" ]]
then
rm "$resolve_file_dir/$resolve_file_name".bak
else
mv "$resolve_file_dir/$resolve_file_name".bak "$resolve_file_dir/$resolve_file_name"
fi
fi
}

die()
{
# "Теги" окрашивания текста -->
local color_start=`echo -e "\033[1;31m"`
local color_end=`echo -e "\033[00m"`
# "Теги" окрашивания текста <--

echo "${color_start}Error! $@$color_end" >&2
cleanup
exit 1
}

interrupt_handler()
{
if [[ "$is_in_critical_section" == "" ]]
then
echo "Script interrupted." >&2
cleanup
exit
fi
}
# Функции <--



# Script start

is_in_critical_section=""

# Проверяем правильность исходных данных -->
resolve_file="$1"
if [[ "$resolve_file" == "" || "$2" != "" ]]
then
die "Usage: resolve_conflict.sh /path/to/file"
fi

if [[ ! -f "$resolve_file" ]]
then
die "File '$resolve_file' not exists."
fi

if [[ -e "$resolve_file".bak ]]
then
die "Please remove '$resolve_file.bak' file."
fi

if [[ -e "$resolve_file".new ]]
then
die "Please remove '$resolve_file.new' file."
fi
# Проверяем правильность исходных данных <--

# Проверяем наличие необходимых приложений -->
which cvs > /dev/null || die "cvs must be installed."
which meld > /dev/null || die "meld must be installed."
# Проверяем наличие необходимых приложений <--

# Получаем родительскую дирректорию файла
# и имя самого файла -->
resolve_file_dir=$(dirname "$resolve_file") || die
cd "$resolve_file_dir" || die
resolve_file_dir="$(pwd)" || die

resolve_file_name=$(basename "$resolve_file") || die
# <--

# Устанавливаем обработчики прерываний -->
trap interrupt_handler SIGHUP
trap interrupt_handler SIGINT
trap interrupt_handler SIGQUIT
trap interrupt_handler SIGTERM
# Устанавливаем обработчики прерываний <--

# Создаем временную директорию
temp_dir=$(mktemp -d /tmp/resolve_cvs_file_XXXXXX) || die

cp -r "$resolve_file_dir"/CVS "$resolve_file_dir/$resolve_file_name" "$temp_dir" || die

cd "$temp_dir" || die

# Получаем во временную директорию все необходимые файлы из CVS -->
# cvs не возвращает кода ошибки, если, например, такого файла не существует,
# поэтому проверяем, появился он или нет.

# Сохраняем резервную копию CVS/Entries
cp CVS/Entries CVS/Entries.bak || die

mv "$resolve_file_name" mine."$resolve_file_name" || die

# BASE -->
cp CVS/Entries.bak CVS/Entries || die
cvs update -r BASE "$resolve_file_name" > /dev/null 2>&1
cp CVS/Entries CVS/Entries.base || die

if [[ ! -e "$resolve_file_name" ]]
then
die "CVS error."
fi

mv "$resolve_file_name" base."$resolve_file_name" || die
# BASE <--

# HEAD -->
cp CVS/Entries.bak CVS/Entries || die
cvs update "$resolve_file_name" > /dev/null 2>&1
cp CVS/Entries CVS/Entries.head || die

if [[ ! -e "$resolve_file_name" ]]
then
die "CVS error."
fi

mv "$resolve_file_name" head."$resolve_file_name" || die
# HEAD <--
# Получаем во временную директорию все необходимые файлы из CVS <--

# Запускаем meld и, если это необходмо, сохраняем изменения в файле -->
resolve_file_mtime=$(stat -c'%y' base."$resolve_file_name") || die
meld head."$resolve_file_name" base."$resolve_file_name" mine."$resolve_file_name" > /dev/null 2>&1
new_resolve_file_mtime=$(stat -c'%y' base."$resolve_file_name") || die

if [[ "$resolve_file_mtime" != "$new_resolve_file_mtime" ]]
then
echo "You had changed file. Do you want to save it? [y/N]"
read user_answer

if [[ "$user_answer" == "y" || "$user_answer" == "Y" ]]
then
# Заменяем старый файл новым, попутно обновляя информацию о нем в CVS -->
# Входим в "критическую секцию", внутри которой скрипт прерывать нельзя
is_in_critical_section="yes"
echo "Saving file..."

cd "$resolve_file_dir" || die

# Создаем бэкап
mv "$resolve_file_name" "$resolve_file_name".bak || die

# На первый взгляд это лишняя операция, т. к. мы могли
# бы сразу переместить файлы из временной папки, а не
# выполнять две операции - копирование и перемещение.
# Но так мы гарантируем, что у нас хватит места для
# наших файлов на диске.
# -->
if ! cp "$temp_dir"/base."$resolve_file_name" "$resolve_file_name".new
then
if [[ -e "$resolve_file_name".new ]]
then
rm "$resolve_file_name".new
fi

die
fi

if ! cp "$temp_dir/CVS/Entries.head" CVS/Entries.new
then
if [[ -e CVS/Entries.new ]]
then
rm CVS/Entries.new
fi

die
fi
# <--

# Делаем бэкап CVS/Entries
mv CVS/Entries CVS/Entries.bak || die

# Обновляем CVS данные о нашем файле
#  -->
if ! mv CVS/Entries.new CVS/Entries
then
mv CVS/Entries.bak CVS/Entries \
|| echo "Error! Can't move CVS/Entries.bak to CVS/Entries. \
Please move it manually. Otherwise your CVS sand-box will break."

die
fi
#  <--

rm CVS/Entries.bak

# Теперь мы имеем файл такой версии, с которой мы уже разрешили конфликт.
# Поэтому просто заменяем его нашим новым файлом.
mv "$resolve_file_name".new "$resolve_file_name" || die

echo "File '$resolve_file_dir/$resolve_file_name' has been saved."
# Выходим из "критической секции"
is_in_critical_section=""
# Заменяем старый файл новым, попутно обновляя информацию о нем в CVS <--
fi
else
echo "No changes has been applyed to file."
fi
# Запускаем meld и, если это необходмо, сохраняем изменения в файле <--

cleanup

echo "Exiting..."

Комментариев нет: