Рефакторинг проекта в SVN с помощью ANT

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

Предыстория

Имеется проект в SVN из 15 000 файлов и 5 000 папок. Проекту почти 10 лет, на нем поработало несколько поколений разработчиков разной квалификации. В какой-то момент, пару-тройку лет назад, а может и раньше, архитектура проекта «потекла». Разные модули и слои стали писаться в разных стандартах организации кода, возникли циклические зависимости между модулями. В итоге в SVN за долгие года образовалась свалка. Проект собирается, но совершенно шаманским способом.

Задача

Привести код к единому формату хранения. При этом сохранить историю изменений по каждому файлу и не останавливать процесс разработки.

Сложности

Сохранить историю по одному файлу или папке в SVN довольно просто с помощью команды svn copy. При небольшом количестве файлов все можно сделать вручную.
С разбором большого проекта сложно. Пока будешь вручную разбирать 15 000 файлов, разработчики накоммитят новых изменений и их тоже нужно будет копировать. Замкнутый круг.
Нужна автоматизация. Скриптик, который раз! — и переводит проект в новую структуру.

Результат

Задача была выполнена, а побочным продуктом стал подход к написанию ANT-скриптов, который в большом программировании называется инкапсуляция. Хочу поделиться полученным подходом.


Исходные коды (значительно укороченные, без названий компании и продукта) доступны на гуглокоде.
  • mapping.xml — маппинг, откуда и куда копировать, по какому правилу
  • old2new.properties — глобальный конфиг.
  • macros.ant.xml — подключаемый ant-файл с реализацией копирования в файловой системе
  • macros.svn.xml — подключаемый ant-файл с реализацией копирования с svn copy.


История того, как я пришел к таким скриптам.

Составил план действий:
  1. придумать новую структуру
  2. написать скрипт для копирования в файловой системе проекта из старой структуры в новую
  3. отладиться на новой структуре, написать скрипты сборки и т.п. — отладка может занять много времени и десятков (если не сотен) запусков операций копирования в файловой системе
  4. написать скрипт, который готовит скрипт с svn copy
  5. отладиться на новой структуре в svn
  6. объявить час Х, прогнать скрипт и перевести всех разработчиков на новую структуру.


Реализация плана действий:

Описание новой структуры из пункта один выходит за рамки данной статьи.
Скрипт для копирования можно писать на bat, bash.
Поскольку у нас используется средство сборки ANT, то я начал писать его на нем.
В моем случае для написания ANT-скрипта и получения первого успешного результата потребовалось пару недель.

Скрипт состоял из команд такого вида:
       <mkdir dir="@{to}"/>
            <copy todir="@{to}" overwrite="false">
                <fileset dir="@{from}">
                    <include name="@{include}"/>
					<include name="@{include}"/>
					<exclude name="@{include}"/>
                </fileset>
            </copy>


Он получился очень длинным, плохо читаемым и модифицируемым.
У него было одно достоинство — он работал. Но предстояло писать подобный файл для svn copy… А это несколько напрягало.
После напряженных раздумий, решил, что надо писать по-другому. Избавляться от copy-paste, выносить в отдельный файл маппинг и прятать реализацию в подключаемых файлах. Но как это сделать в ANT? С помощью макросов для ANT!

C помощью директивы macrodef определяем новый таск в ant.macros

    <macrodef name="copy_by_pattern">
        <attribute name="from"/>
        <attribute name="to"/>
        <attribute name="include"/>
        <sequential>
            <echo message="copy from dir @{from} to dir @{to} by pattern @{include}"/>
            <mkdir dir="@{to}"/>
            <copy todir="@{to}" overwrite="false">
                <fileset dir="@{from}">
                    <include name="@{include}"/>
                </fileset>
            </copy>
        </sequential>
    </macrodef>


Таск copy_by_pattern копирует файлы по маске include.

Код выше преобразуется в

       <copy_by_pattern from="${dir.input.base}/module1"
                         to="${dir.output.base}"
                         include="**"/>
        <copy_by_pattern from="${dir.input.base}/module2"
                         to="${dir.output.base}"
                         include="**"/>
        <copy_by_pattern from="${dir.input.base}/module3"
                         to="${dir.output.base}"
                         include="**"/>


Добиваемся того, чтобы он работал как и до этого.
А потом пишем реализацию copy_by_pattern для svn copy в svn.macros

    <macrodef name="copy_by_pattern">
        <attribute name="from"/>
        <attribute name="to"/>
        <attribute name="include"/>
        <sequential>
            <fileset id="localfs" dir="@{from}">
                <include name="@{include}"/>
            </fileset>
            <local name="out.script"/>
            <pathconvert property="out.script" refid="localfs" pathsep="${line.separator}">
                <chainedmapper>
                    <identitymapper/>
                    <regexpmapper from="@{from}/(.*)"
                                  to="${cli.command} @{from}/\1       @{to}/\1"
                                  handledirsep="yes"/>
                </chainedmapper>
            </pathconvert>
            <echo file="${script.filename}"
                  message="${out.script}${line.separator}"
                  append="true"/>
        </sequential>
    </macrodef>	


Этот таск получает файлсет и конвертирует его локальную переменную (появились в ANT 1.8) out.script с помощью pathconvertи regexpmapper. Затем выводит фрагмент скрипта с svn copy в файл ${script.filename}

Примечание: в ANT до версии 1.8 не было локальных переменных. Записав в переменную значение, его потом нельзя изменить.

Пример фрагмента скрипта с svn copy, получаемого на выходе:

svn copy --parents D:\path_old\app.xml D:\path_new\app.xml
svn copy --parents D:\path_old\Cls.java D:\path_new\Cls.java
svn copy --parents D:\path_old\page.jsp D:\path_new\page.jsp
svn copy --parents D:\path_old\js.js D:\path_new\js.js

ключ --parents создаст все необходимые новые директории

Замечание о команде svn copy

У нее есть 4 варианта копирования:
WC -> WC: copy and schedule for addition (with history)
WC -> URL: immediately commit a copy of WC to URL
URL -> WC: check out URL into WC, schedule for addition
URL -> URL: complete server-side copy; used to branch and tag


Наиболее удобен вариант WC -> WC:, потому что все остальные выполняют немедленный коммит в репозиторий. Перетасовка 15 000 файлов проекта создаст 15 000 коммитов. Вариант WC -> WC позволяет делать один отложенный коммит.

Заключение

Пересмотрите свои ANT-скрипты.
Если вы видите в своих ANT-скриптах много повторяющихся фрагментов,
их можно объединить в новый таск с помощью macrodef.
А заодно можете разделить логику от реализации.
  • +15
  • 1.7k
  • 4
Share post

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 4

    +1
    Еще сразу замечание, для тех кто будет пользоваться скриптами на практике. Важно сразу определиться с форматом разделителя директорий в путях. \ или /. Везде использовать один и тот же вариант.
    Сам ANT к формату безразличен, но regexp-mapper чувствителен.
      +1
      macrodef-ы удобно использовать совместно с пространствами имен, тогда вместо <copy_by_pattern /> было бы что-то вроде <my:copy /> + если в файле содержатся только макросы в качестве корневого тега можно использовать antlib.

      Примечание: в ANT до версии 1.8 не было локальных переменных. Записав в переменную значение, его потом нельзя изменить.

      Неудобство обходится достаточно легко созданием уникальных свойств для каждой копируемой директории:
      <pathconvert property="out.script.@{from}" refid="localfs" pathsep="${line.separator}" />
      


      + очень важно помнить о том, что @{from} не является переменной и при вызове макроса просто заменяется переданной строкой, это может приводить к проблемам при использовании совместно с задачей script, в этих случаях лучше использовать scriptdef.
        0
        Неудобство обходится достаточно легко созданием уникальных свойств для каждой копируемой директории:

        <pathconvert property="out.script.@{from}" refid="localfs" pathsep="${line.separator}" />
        



        Вначале пытался обойтись без локальных переменных, но
        1) скрипт становиться более громоздким. В моем случае придется генерить уникальный property, используя все три параметра @{from} @{to} @{pattern}
        2) property, в имени которого содержится спец символы (звездочки, слеши) как-то криво работает. Подробностей не помню, но стал искать, как это можно обойти.
        Нашел локальные переменные.
        0
        AntCall позволяет создавать targets с параметрами. Можно использовать вместо локальных переменных.

        Only users with full accounts can post comments. Log in, please.