UnixMountainSkiFun

Unix Горы Лыжи

02-06-2008 16:54

Поддержка


Представим простейшую undernet-команду (тут я хочу сказать спасибо Starkey Laboratories, превосходной компании по производству слуховых аппаратов в Eden Prairie, Minnesota, за предоставление части их undernet, в создании которого я участвовал зимой 2000-2001 годов). Он выглядит примерно так, как показано на Рисунке 6-1. В рамках этого undernet, мы видим индивидуализацию, поднятую на новый уровень. Ранее мы уже использовали CVS (Concurrent Versioning System, -- Совместную Систему Контроля Версионности), для того, чтобы управлять исходным кодом некоторых проектов. Одной из причин, по которой мы решили использовать этот инструментарий, явилось то, что когда мы настроили undernet, стало ясно, что необходимо выполнять backup (архивирование) контента, а сетевые администраторы слишком ленивы, чтобы поместить персональные компьютеры в свои схемы по backup-у. Тогда я сказал, -- "No problem, я загоню весь Web-сайт в CVS-репозитарий на одном из основных Unix-серверов".

Рисунок 6-1. Konquerer, отображающий домашнюю страничку undernet.

Тут надо сделать небольшое отступление от истории с undernet, и сделать вводную в CVS.

Основы CVS

Concurrent Versioning System -- очень мощная и законченная система управления версиями (version-control system). Она имеет обширный функционал и большое количество опций, и позволяет эффективно использовать ее мощь в различных вариантах. CVS сама по себе достойна целой книги, -- по крайней мере одной из таких книг, наиболее часто используемой, является свободно распространяемая Guide to Using CVS, от Cederqvist, -- она упоминается этой главе в разделе Выводы, URL-ы, библиография. Тем не менее, мы не оставим вас наедине с этой книгой. В этой главе мы приведем достаточно вводной информации для того, чтобы Вы смогли начать пользоваться CVS.

Основные концепции

CVS это еще один пример хорошо известной модели клиент/сервер (client/server model). Имеется CVS-сервер, который управляет централизованным архивом файлов, называемым репозитарием (repository), и от одного до нескольких клиентов, которые работают в рамках рабочих копий для каждого пользователя, такие копии нам нравится называть песочницами (sandboxes), так как это то место, где каждый из пользователей может спокойно "возиться" со своей собственной копией файлов, которые изначально управляются с помощью CVS, причем все это делается так, что никоим образом не мешает всем остальным пользователям.

Пользователь может выполнить операцию checkout над песочницей (прим.переводчика: взять файлы в свою песочницу). Кроме того, пользователь еще в состоянии выполнить commit изменений в репозитрий (прим.переводчика: передать изменения из песочницы в репозитарий) и update для своей песочницы (прим. переводчика: обновить файлы в песчнице), чтобы увидеть изменения, которые были сделаны в репозитарии другими пользователями, с момента последнего checkout или update.

Иногда может случиться так, что другой пользователь изменил файл, который изменял и наш пользователь, в своей собственной песочнице. Когда такое случается, то говорят, что произошла коллизия (collision), и тот, кто производит commit последним, должен выполнить merge (слияние). В большинстве случаев CVS в состоянии выполнить это самостоятельно. Однако если два и более пользователей сделали изменения над одной и той же частью файла, то в таком случае происходит конфликт (conflict), и лицо, выполняющее commit, должно вручную разрешить такой конфликт.

Понимаю, что трудно сразу понять и освоить все то, о чем только что упомянул, однако мы приведем команды, и объясним для какой из концепций применима каждая из них. Мы приведем примеры каждой из этих концепций с кратким объяснением. Главное не бояться! CVS действительно работает. Она используется в большинстве OpenSource-проектов, таких как wine или plex86, а также во многих других, и позволяет сотням разработчиков, большинство из которых никогда и не видели друг друга, одновременно работать с "тучей" исходного кода. Даже эта книга была написана с использованием CVS для того, чтобы координировать изменения, вносимые в нее несколькими авторами. А посему расслабьтесь и дайте нам возможность показать кое-что из того, что можно сделать с помощью CVS.

Репозитарий

Репозитарий -- это основа для всех файлов, управляемых с помощью CVS. CVS-сервер может управлять от одного до нескольких репозитариев. Большинство команд CVS-клиента сопровождаются должен сообщать указанием того, с каким репозитарием работать. Вы можете указывать репозитарий в командной строке, вместе с каждой из команд, однако более общим подоходом к работе является указание репозитария в переменную окружения CVSROOT. Репозитарий может быть локальным, в этом случае серверная часть CVS не нужна, -- клиент просто использует локальную файловую систему. Тогда CVSROOT будет представлять собой обычный "путь" (pathname), например:

 export CVSROOT=/usr/local/secureprojects

Репозитарий не обязан быть локальным. Он может быть выполнен различными способами. Наиболее общей формой удаленного доступа является следующий:

 :<method>:<user/host spec>:<pathspec>

где method одно из следующих:

  • pserver: Это CVS-овский клиент-серверный протокол.
  • ext: Этот протокол используется внешней программой, которая может вызывать CVS-команды на удаленном компьютере. Программой по-умолчанию является rsh (remote shell -- удаленный командный процессор). Однако, CVS будет использовать, для выполнения команд, любую другую программу, указанную в переменной окружения CVS_RSH. Наиболее общим использованием этой переменной является указание использовать secure shell (ssh/OpenSSH), вместо небезопасного RSH-протокола.
  • kserver: Это безопасный протокол Kerberos version 4. Мы не будем рассматривать его в этой главе.
  • gserver: Это протокол Generic Security Services API и/или Kerberos version 5. Этот вариант мы также не будем рассматривать.

Строка user/host spec следует login-спецификации rsh/ssh:

 user@some.hostname.org

Эта форма используется как при использовани pserver, так и ext.

Строка pathspec -- это путь к репозитарию на указанном хосте.

CVS-команды

Хотелось бы более детально рассмотреть CVS-команды. К сожалению, здесь мы сможем предложить только лишь беглый очерк и предложить обратиться к ресурсам, излагающим суть CVS более полно. Наиболее общей формой вызова CVS-клиента является следующая:

 cvs cvs-options cvs-command cvs-command-options [filespecs]

Наиболее интенсивно используемые CVS-команды представлены в следующей ниже таблице. Конечно же эта таблица никоим образом не претендует на полный перечень CVS-команд, она даже и близко не стоит с полной документацией по командам. Тем не менее, их должно быть достаточно для того, чтобы начать работу.

CVS-команды Описание и примеры
import Команда import используется для того, чтобы разом перенести целое дерево подкаталогов в репозитарий. Эта команда обычно используется для первичного переноса, существующего проекта, в репозитарий. Она может быть полезна когда проект начинается "с нуля", однако если по ходу работы над проектом потребуется добавлять файлы и/или каталоги, то более правильным будет использовать команду add.

Пример: $ cvs import book addison_wesley initial

Первый аргумент команды import, приведенной выше, это название каталога, который будет занесен в репозитарий. Внимание! Эта команда не похожа ни на какую другую CVS-команду! Команда import начнет рекурсивно обрабатывать текущий каталог, и все файлы, находящиеся в этом каталоге, будут размещены в репозитарии, в каталоге, который указан, как первый аргумент команды. По-умолчанию, большинство CVS-команд работают с указанным каталогом, и всеми его подкаталогами (если противное не указано). Вторым аргументом является метка владельца (vendor tag). Использование этого аргумента не входит в это рассмотрение, однако, к сожалению, вы должны указывать его, даже если никогда им и не воспользуетесь. После метки владельца, должна быть указана как минимум одна начальная метка ревизии (revision tag). Метки ревизии это способ именования целого набора файлов, с разными ревизионными номерами, таким образом Вам не потребуется помнить различные ревизионные номера различных файлов, которые должны составить выпускаемый вами продукт. И снова, возможно эта начальная метка и не потребуется, так как все ваши файлы скорее всего начнутся с ревизии 1.1, однако команда требует наличия этого аргумента. Большинство пользователей могут использовать всего лишь пару-тройку меток, и больше "не заморачиваться" на эту тему. Обратитесь к CVS-ресурсам, если пожелаете знать больше на эту тему.

checkout Эта команда создает песочницу. В отличие от других систем управления исходными текстами, CVS не блокирует файлы. Вы можете выполнять checkout хоть всего проекта целиком, и полученная копия Ваша, -- делайте с ней, все что заблагорассудится. Однако, если вы пожелаете внести изменения, которые потом будут доступны другим, и станут частью ревизионной истории проекта, то вы должны использовать команду commit, чтобы внести изменения в репозитарий. Если же другие пользователи сделали изменения в файлах, то уже вы должны использовать команду update, чтобы "подтянуть" их изменения в свою песочницу.

Пример: $ cvs checkout book

commit Если вы вносите некоторые изменения в файлы в своей песочнице, а потом желаете, чтобы эти изменения стали частью истории репозитария, то необходимо выполнить commit изменений в репозитарий. Это приведет к тому, что версии файлов вашей песочницы, станут постоянной и неотъемлимой частью истории, хранимой в репозитарии. Команда commit может закончиться неудачей, если другой пользователь сделал изменения в одном из тех же файлов, которые недавно были получены вами с помощью checkout или update. Чтобы разрешить конфликт, потребуется использование команды update. Когда вы посылаете команду commit, открывается предпочитаемый вами редактор, и вы можете внести комментарии, описывающие изменения, которые станут частью CVS-журнала (CVS log), для каждого измененного файла.

Пример: $ cvs commit

update Команда update приводит вашу песочницу в соответствие с изменениями, сделанными в репозитории другими пользователями. Если вы локально изменили некоторые из файлов, то изменения из репозитария автоматический сольются с вашими изменениями. Обычно это происходит автоматически. Однако, иногда изменения других пользователей могут быть в тех же самых частях файлов, что и у вас. И когда такое случается, то команда update сообщит о том, что произошел конфликт слияния, и что вам потребуется разрешить его. Смотрите следующий раздел, -- Разрешение конфликтов.

Пример: $ cvs update

add Команда 'add позволяет добавлять новые файлы в репозитарий. Команда add просто помечает файл или файлы на включение в репозитарий. После выполнения этой команды, потребуется вызвать команду commit'', чтобы поместить файлы в репозитарий.

Пример: $ cvs add newfile.java newdir newdir/newpage.html
Как видите, добавление каталогов происходит тем же самым способом.

remove Команда remove "удаляет" файлы и/или каталоги из репозитария. Это команда просто помечает файлы на удаление. Потребуется вызвать команду commit чтобы действительно удалить их из репозитария. Причем потребуется удалить файл из вашей песочницы до того, как вы выполните команду remove. Эта команда не удаляет файл! Помните, что CVS это версионная система, которая может привести/вернуть набор файлов в любое из первоначальных состояний, а это означает, что "удаление" может рассматриваться как лишение такого файла "будщего". Файлы, которые удалены перемещаются в особый каталог репозитария, называемый Attic (чердак). Таким образом, файл исчезает из текущих и будущих update-ов и checkout-ов, однако файл все же может быть доступен при просмотре истории репозитария.

Пример: $ rm droppedfile.java $ cvs remove droppedfile.java

Разрешение конфликтов

Обычно CVS способен выполнять слияние файлов, измененных вами, с изменениями, сделанными другими пользователями, незаметно и молчаливо. Давайте отследим два типа изменений в файле, -- первый тип, когда изменения другого пользователя не конфликтуют с нашими, и второй тип, когда налицо конфликт; тем самым мы сможем продемонстрировать то, как работает разрешение конфликтов.

Вот исходное состояние нашего просто файла:

 This is a sample text file that will
 allow us to demonstrate how
 CVS can allow many users to work
 on the same files at the same
 time. Not only that, but it will
 show how users may merge changes
 and how sometimes conflicts will
 occur, and how those conflicts can
 be resolved.

 Enjoy.

Наш первый сценарий покажет безконфликтное слияние изменений файла двумя пользователями. Допустим, что первый пользователь сделал следующие изменения и за-commit-ил их:

 This is a sample text file that will
 allow us to demonstrate how
 CVS can allow many users to work
 on the same files at the same
 time. Not only that, but it will
 show how users can merge changes
 and how sometimes conflicts will
 occur, and how those conflicts can
 be resolved.

 Enjoy.

Его сессия работы с CVS-командами может выглядеть таким образом:

 first@mars:~/user1/project$ cvs commit
 cvs commit: Examining .
 Checking in sample.txt;
 /usr/local/projects/project/sample.txt,v  <--  sample.txt
 new revision: 1.2; previous revision: 1.1
 done
 first@mars:~/user1/project$

Предположим, что второй пользователь, ничего не зная о изменениях, сделанных первым пользователем, изменил свою копию этого файла в песочнице вот таким образом:

 This is an example text file that will
 allow us to demonstrate how
 CVS can allow many users to work
 on the same files at the same
 time.  Not only that, but it will
 show how users may merge changes
 and how sometimes conflicts will
 occur, and how those conflicts can
 be resolved.

 Enjoy.

Теперь, пользователь №2, сделав эти изменения, пытается выполнить commit своих изменений. Этот процесс оканчивается ошибкой, показанной ниже:

 second@mars:~/user2/project$ cvs commit
 cvs commit: Examining .
 cvs commit: Up-to-date check failed for 'sample.txt'
 cvs [commit aborted]: correct above errors first!
 second@mars:~/user2/project$

Это сообщение о ошибке говорит нам о том, что файл sample.txt был изменен с момента последнего checkout или update. CVS-команда update приводит нашу песочницу в состояние соответствующее последнему состоянию репозитария. Вы можете подумать, что данные из репозитария перезапишут все наши изменения, -- однако это не так. Если мы имеем файлы, измененные локально, то команда update вольет в наши файлы изменения других пользователей. Это будет выглядеть примерно так:

 second@mars:~/user2/project$ cvs update
 cvs update: Updating .
 RCS file: /usr/local/projects/project/sample.txt,v
 retrieving revision 1.1.1.1
 retrieving revision 1.2
 Merging differences between 1.1.1.1 and 1.2 into sample.txt
 M sample.txt
 second@mars:~/user2/project$

Теперь посмотрим на содержимое файла:

 second@mars:~/user2/project$ cat sample.txt
 This is an example text file that will
 allow us to demonstrate how
 CVS can allow many users to work
 on the same files at the same
 time.  Not only that, but it will
 show how users can merge changes
 and how sometimes conflicts will
 occur, and how those conflicts can
 be resolved.

 Enjoy.

 second@mars:~/user2/project$

Как видите, изменения обоих пользователей присутствуют в файле. Но тем не менее, эти изменения все еще локальны. Помните, -- наш последний commit "не прошел", поэтому мы должны его выполнить снова.

 second@mars:~/user2/project$ cvs commit
 cvs commit: Examining .
 Checking in sample.txt;
 /usr/local/projects/project/sample.txt,v  <--  sample.txt
 new revision: 1.3; previous revision: 1.2
 done
 second@mars:~/user2/project$

Это было легкое введение суть дела в рамках нашей дискуссии о конфликтах слияния. Вспомним, -- наш второй пользователь модифицировал первую строку файла, а первый пользователь ни модифицировал его, ни выполнял update (для того, чтобы увидеть изменения второго пользователя). Теперь предположим, что наш первый пользователь изменил первую строку файла примерно так:

 This is a sample flat ASCII file that will
 allow us to demonstrate how
 CVS can allow many users to work
 on the same files at the same
 time.  Not only that, but it will
 show how users can merge changes
 and how sometimes conflicts will
 occur, and how those conflicts can
 be resolved.

 Enjoy.

Теперь наш первый пользователь пытается выполнить commit, однако получает то же самое сообщение о ошибке, которое уже видел второй пользователь, говорящее о том, что копия этого файла в репозитария изменилась:

 first@mars:~/user1/project$ cvs commit
 cvs commit: Examining .
 cvs commit: Up-to-date check failed for 'sample.txt'
 cvs [commit aborted]: correct above errors first!
 first@mars:~/user1/project$

Точно так же как и раньше, наш первый пользователь должен использовать команду ipdate, чтобы привести песочницу в единообразное состояние с репозитарием:

 first@mars:~/user1/project$ cvs update
 cvs update: Updating .
 RCS file: /usr/local/projects/project/sample.txt,v
 retrieving revision 1.2
 retrieving revision 1.3
 Merging differences between 1.2 and 1.3 into sample.txt
 rcsmerge: warning: conflicts during merge
 cvs update: conflicts found in sample.txt
 C sample.txt
 first@mars:~/user1/project$

Но что это? Похоже эта картинка несколько отличается от того, когда мы последний раз выполняли слияние с изменениями другого пользователя. В чем дело? Все из-за того, что оба изменения были сделан над одной и той же частью файла. CVS не может понять каким образом выполнить слияние без потери чьей-либо работы. Что же тогда делать? А давайте посмотрим на файл в песочнице первого пользователя:

 first@mars:~/user1/project$ cat sample.txt
 <<<<<<< sample.txt
 This is a sample flat ASCII file that will
 =======
 This is an example text file that will
 >>>>>>> 1.3
 allow us to demonstrate how
 CVS can allow many users to work
 on the same files at the same
 time.  Not only that, but it will
 show how users can merge changes
 and how sometimes conflicts will
 occur, and how those conflicts can
 be resolved.

 Enjoy.

 first@mars:~/user1/project$

Опля! Что это за мусор в файле? CVS "разметила" файл. Она просто не может решить чьи изменения должны превалировать, а поэтому вывалила обе версии в песочницу, чтобы кто-нибудь смог решить проблему и выполнить commit. Она как бы говорит, -- "Извини дружок, -- это твоя проблема". Пользователь должен подредактировать файл, решая самостоятельно как решить проблему меж версиями. Помимо этого, пользователь должен удалить и разметку, которую CVS поместил в файл.

Итак, первый пользователь должен изменить файл, чтобы он выглядел примерно так:

 This is an example flat ASCII file that will
 allow us to demonstrate how
 CVS can allow many users to work
 on the same files at the same
 time.  Not only that, but it will
 show how users can merge changes
 and how sometimes conflicts will
 occur, and how those conflicts can
 be resolved.

 Enjoy.

Теперь первый пользователь может выполнить commit:

 first@mars:~/user1/project$ cvs commit
 cvs commit: Examining .
 Checking in sample.txt;
 /usr/local/projects/project/sample.txt,v  <--  sample.txt
 new revision: 1.4; previous revision: 1.3
 done
 first@mars:~/user1/project$

Вот как вы будете разрешать CVS-конфликты!

Это всего лишь очень краткое введение в CVS. Мы не рассмотрели такие понятия как tagging, branching, branch merges, diffs, status, да и множество других вещей. CVS -- это мощный и многофункциональный инструментарий. Нашей целью было всего лишь сделать краткое введение в этот инструментарий.

А теперь вернемся к нашим баранам

Когда последний раз мы толковали о undernet, я предложил хранить критические части системы на одном из основных Unix-серверов, чтобы ни одна критичная часть контента не была потеряна в случае какого-либо сбоя в той части undernet, которая работает на Linux PC.

Это предложение оказалось приемлимым для нашего клиента, и это в свою очередь привело к неожиданным и значительным преимуществам от внедрения проекта.

Перед тем, как мы перейдем к изложению этих преимуществ, дайте-ка я изложу как мы все настраивали. Для начала, мы сформировали очень простую (по большей части HTML 2.0) домашнюю страничку. Мы устанавливали undernet на системе Debian, а посему файл index.html находился в /var/www. Содержимое этого файла приведено ниже.

 <HTML>
 <HEAD>
 <TITLE>Frigate</TITLE>
 </HEAD>
 <BODY BGCOLOR="#F0F8FF">
 <TABLE>
 <TR>
 <TD>
 <IMG SRC="see/images/senses_n_star.jpg" ALT="project logo"
 HEIGHT=100
 WIDTH=100
 ALIGN=BOTTOM>
 </TD>
 <TD>
 <H1>CQ Messenger - Project Homepage</H1>
 </TD>
 </TR>
 </TABLE>
 <P>
 <H2>Run CQ Messenger: (Iter. 1)</H2>
 <UL>
     <li>in DEVELOPMENT (frigate)
     <TABLE cellpadding=10 cellspacing=10><TR>
         <td><a href="/servlet/starkey.track.ui.CQMessenger">
         in a full browser</a></td>
         <td><a href="http://frigate.starkey.com/see/CQMessenger.html">
         like a real user (bare window)</a></td>
         <td><a href="/servlet/starkey.track.ui.CQMessenger?admin=users">
         USER status</a></td>
         <td><a href="/servlet/starkey.track.ui.CQMessenger?admin=boxes">
         BOX status</a></td>
         <td><a href="/servlet/starkey.track.ui.CQMessenger?admin=show">
         LOG Settings</a></td>
     </TR></TABLE><BR>
     <li>in PRODUCTION (cqm a.k.a. hulk)
     <TABLE cellpadding=10 cellspacing=10><TR>
         <td><a href="http://cqm.starkey.com/see/CQMessenger.html">
         CQ Messenger</a></td>
         <td><a href="http://cqm.starkey.com/servlet/starkey.track.ui. CQMessenger?admin=users">
         USER status</a></td>
         <td><a href="http://cqm.starkey.com/servlet/starkey.track.ui. CQMessenger?admin=boxes">
         BOX status</a></td>
     </TR></TABLE>
 </UL>
 <HR>
 <H2>CQ Messenger Project Documentation</H2>
 <UL>
         <li><a href="/cgi-bin/prbarcode.cgi">
         <H3><img src="code39.gif" HEIGHT="50" WIDTH="50">
         Print a barcode</H3></a>
         <li><a href="/CQ/docs/">Project Documents Repository (copy)</a>
         <li><a href="http://frigate.starkey.com/cgi-bin/cvsweb">CVS Repository (read-only)</a>
         <li><a href="http://cq-dev.starkey.com/see/XFiles/index.html">
                                 CQ Messenger Object classes</a>
         <p>
         <li><a href="/javadoc/">Sun Java documentation</a>
         <li><a href="/javadoc/api">JDK1.2.2 Class Documentation</a>
         <p>
 </UL>
 <HR>
 <H2>Frigate search engine</H2>
 <p>
 Enter search keywords below.  Press search button to find matching documents.
 Share and enjoy. This site uses the freely available ht://Dig search
 engine.</p>
 <p>
 </p>
 <form method="post" action="cgi-bin/htsearch">

 Keyword(s): <input type=text maxlength=80 name=words size=25>Match:
 <select name="method">
 <option value="and" selected>All
 <option value="or">Any
 <option value="boolean">Boolean
 </select>
  Format:
 <select name="format">
 <option value="builtin-long">Long
 <option value="builtin-short" selected>Short
 </select>
  <input type=hidden name="config" value="htdig">
 <input type=submit value="Search" name="SUBMIT">
 </form>
 <HR>
 <H2>Frigate</H2>
 Read <a href="frigateFacts.html">some allegedly interesting information</a> about
 Frigate and how the CQ Messenger Web site is set up
 <hr>
 </BODY>
 </HTML>

Критичная для нас информация располагается по ссылке /CQ/docs. Этот каталог, в иерархии /var/www, был создан с помощью CVS-команды:

 # cd /var/www
 # cvs checkout CQ/docs

Такая последовательность команд создала CVS-песочницу (именно так мне нравится называть рабочие каталоги CVS). Вся документация по нашему проекту управляется CVS-ом. Каждый пользователь имеет свою собственную копию файлов, полученную с помощью checkout. Некоторые из них используют WinCVS на Windows-PC, некоторые имеют учетные записи на Linux-компьютерах, некоторые используют HP/UX, а у некоторых IRIX. Так как мы решили стандартизировать нашу документацию с помощью HTML, то совершенно не важно каким образом она написана, и на какой системе. Некоторые пользователи используют для этого MS Word (но конечно же не я!). Некоторые используют Netscape Composer (и это опять таки не я!). Короче говоря, не важно каким образом формируются файлы.

Каждый пользователь выполняет commit и update над своими копиями файлов по мере необходимости. Все волшебство кроется в том, каким образом документы публикуются на web-сервере. Учетная запись root на компьютере, на котором работает web-сервер, имеет следующий crontab:

 frigate:~# crontab -l
 */11 5-19 * * * cd /var/www/CQ/docs; cvs update -d > /dev/null 2> /dev/null

Это означает, что каждые 11 минут, с 5-00 и до 19-00, копии документов в иерархии web-сервера автоматически синхронизируются с CVS-репозитарием. А это значит, что если пользователь создает или изменяет один из своих документов, то в течении 11 минут от его последнего commit-а такие изменения появятся на web-сайте! Пользователю даже нет нужды иметь учетную запись на компьютере с web-сервером!

В последней части этой главы мы описали то, как предоставить групповой доступ к документам web-сервера. Используя CVS, мы можем предоставить любому количеству пользователей способность наполнять контент web-сервера, однако ни один из них не будет иметь прямого доступа к иерархии web-сервера. Помимо этого, так как CVS -- это версионная система, то мы имеем возможность "откатить" контент web-сервера на любое предыдущее состояние.

<< Индивидуализация | Multi Tool Linux | Итог >>


edit RightSideBar