Миграция проекта со StarTeam в SVN

в 11:59, , рубрики: svn, Программирование, Системы управления версиями

Доброго времени суток!

Кто из вас вообще слышал, что такое StarTeam? Думаю мало кто, в прочем как и я пару месяцев назад.

Я до моего текущего места работы вообще не слышал о таком продукте компании Borland. Если спросить у гугла, то окажется, что данный продукт до сих пор существует и даже развивается, но как вы все догадались, речь пойдёт далеко не о последней версии и даже не о предпоследней. У меня версия 5.3, которая была разработана где-то в 2003 году, а установлена и запущена в оборот здесь в 2004 году. И вот почти 11 лет она работала и решала свои задачи.

У меня как у инициативного специалиста сразу зашевелились волосы от этого старого монстра и было решено мигрировать на что-то более современное, а это или SVN или Git. Выбор пал на SVN, потому что с ним я имею опыт работы, также как и мой коллега, а руководство со всем согласилось. Поиски готового решения привели к Importer for SVN от Palarion, по этому решению есть даже статья на хабре. Но как оказалось всё не слишком просто, у меня версия 5.3, а данный продукт предполагает наличие SDK от версии 2005 года, которое оказалось найти очень проблемно. Даже используя всякие трюки вроде переименования нужных библиотек, я не смог запустить данный импортер.

Что ещё можно было попробовать? Я попробовал поднять новый сервер StarTeam, т.к. он имеет интеграцию с SVN, и подключить имеющийся сервер, но у меня ничего не вышло, потому что, судя по всему, они не совместимы между собой, серверы друг друга так и не увидели.

И что же я сделал? Конечно, я поступил как самый настоящий программист: я написал свой инструмент!

Проектирование

Итак, что в исходных данных? У меня есть:

  1. StarTeam Server 5.3
  2. StarTeam Client 5.3
  3. StarTeam SDK 5.3
  4. SVN-сервер с заданным репозиторием
  5. TortoiseSVN c установленным консольным клиентом
  6. Проект который надо перенести

Знакомство со StarTeam

Я не знаю, как обстоит дело с современным StarTeam, но та версия которая есть у меня — ужасна, потому что тут просто нет как такового понятия — версионности. Каждый файл версируется отдельно, понять, что поместили в один момент с этим файлом нереально, как и вытащить снапшот за какое-то время. А ну и ко всему прочему — эта версия не всегда может адекватно понять, какие файлы изменены(т.е. изменённые файлы видятся 100%, а вот те которые не трогали 50/50 понимаются как изменённые).

Также недавно ещё и появилась проблема с тем, что все комментарии и имена пользователей на русском языке отображаются в виде наборов "?????", что означает, что где-то слетела кодировка (и что ещё интереснее кодировка нигде не указывается в клиенте). Причём почему так произошло никто точно не знает и когда такая проблема началась тоже сказать точно не могут.

Из чего ещё состоит StarTeam, спросите вы? Из БД, которая базируется на MS SQL 2008. Казалось бы можно написать грамотно запрос и вытащить всё что требуется, правда? Но увидев около 100 таблиц с именами типа S01, S02 и набор View к ним решено было даже не разбираться с этим всем, потому что кодировка при прямом запросе в БД не отображается корректно.

Планирование действие

Итак, у нас есть исходный проект в интересном репозитории и конечный репозиторий, который пуст. Проекту от Palarion требуется наличие starteam80.jar, который входит в SDK, но у меня в SDK только starteam53.jar. Скажу сразу, что я на Java до начала этого проекта даже не программировал, ни одной строчки кода. Но тут раз надо, то устанавливаем IDE и пробуем. В качестве IDE я выбрал NetBeans и начал разбирать что внутри starteam53.jar.
Внутри этого пакета целый набор классов который позволяет работать с сервером StarTeam. На сайте Borland есть документация к SDK, что спасает от бессмысленного шатания по огромному количеству классов. Далее пробую сделать простенький проект, который мог бы подключиться к серверу и вернуть список проектов. Спустя 2 часа мучений с библиотеками от StarTeam я добился нужного результата. Теперь становится понятно, что работать со StarTeam через SDK можно, так что надо только определить как перенести всю историю.

Но! Как же теперь перенести историю при всех проблемах StarTeam?

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

Когда делается коммит в StarTeam, то комментарий пишется к каждому файлу, который идёт во время коммита, так что достаточно получить комментарий и автора у первого файла, а остальные уже совпадут.

У StarTeam есть особенность хранения файлов, он их хранит в проекте, в проекте есть файлы и папки, а в папка могут быть ещё файлы и папки и каждый файл имеет версионность.

А ещё, если попросить SDK сделать просто извлечение файла, то StarTeam будет извлекать файлы туда, куда он настроен по умолчанию в клиенте, что меня никак не устроило и нужно сначала извлечь в файловый поток (хорошо, что такая возможность есть), а потом сохранить на диск.

Я придумал следующий путь миграции:

  1. Выгрузить всё из StarTeam по папкам на локальный диск;
  2. Сделать проход по папкам и загрузить всё в SVN, в поле комментария вписать время, когда был сделан коммит в StarTeam.

Выгружать будем по следующей схеме:

  1. Начинаем рекурсивный обход дерева папок и файлов проекта;
  2. Получаем первый файл;
  3. Извлекаем всю его историю;
  4. Делаем проход по истории и раскладываем файлы в папку для экспорта проекта;
  5. Создаём папку с временной меткой по маске «yyyy.MM.dd.HH.mm» (я сначала сделал с точностью до миллисекунды, но, как оказалось, так файлы из одного коммита могут попасть в разные ревизии, что является неправильно, а при таком подходе коллизий и проблем не оказалось);
  6. В папке делаем файл History, куда пишем имя автора, комментарий и временную метку для удобства;
  7. Делаем до тех пор, пока не будет извлечён последний файл.

Извлечение из StarTeam

Итак, NetBeans, Java и 0 опыта на данном языке программирования. Отсутствие опыта разработки на Java меня не пугало, потому что есть гугл, проект разовый и никто не требует от меня высочайших знаний, значит где-то можно просто пожертвовать производительностью/памятью/красотой кода или всем сразу, потому что надо просто сделать.

При извлечении коммитов я столкнулся с проблемой того, что авторы коммита возвращаются в виде ID, а не в виде строки, что меня удивило. Поиск по документации показал, что авторов получить можно, но в виде списка ID+Имя, но у меня проблемы с кодировкой и я не могу прочесть ряд пользователей и тут почему-то есть ряд дублей, а в SVN дубли заводить не планировалось. Я нашёл в БД нужную вьюшку и оттуда по e-mail и методом исключения уставновил какому автору какой ID принадлежит. Вот тут и есть самый большой костыль: я написал возврат имени автора, а также пароль и пользователей через switch-case, да глупо и неоптимально, но при моих проблемах тут уж никуда не деться.

Несмотря на отсутствие опыта по работе с данным языком программирования, но писать плохой код я не собирался и получилось вот так:

Исходный код извлечения данных из StarTeam

        Server StarTeamServer = new Server("WINAPPSRV", 49201);
        StarTeamServer.connect();
        
        if (StarTeamServer.isConnected()) {
            System.out.println("Connect to server OK!");
            StarTeamServer.logOn("markov", "123456"); 
            
            if (StarTeamServer.isLoggedOn()) {
                System.out.println("LogOn to server OK!"); 
               
                Project[] projects = StarTeamServer.getProjects();
                Project TW = null;
                for (Project currentproject : projects) {                    
                    if (currentproject.getName().equals("Tw")) {
                        TW = currentproject;
                        break;
                    }
                } 
                
                if (TW != null) {
                    System.out.println("Try to find first revision");
                    
                    View CurrentView =  TW.getDefaultView();

                    //Путь до точки назначения должен быть с / в конце
                    ExtractFullTreeFromRoot(CurrentView.getRootFolder(), "/", "C:/StarTeamToSVN");
                } else {
                    System.out.println("Project Tw not found in StarTeam repository");
                }
            } else {
                System.out.println("LogOn to server failed :'(");
            }
            
            StarTeamServer.disconnect();

Исходный код функций

private static void ExtractFileHistory(com.starbase.starteam.File SourceFile, String SourceFolderName, String RootFolder) {
        Item[] FileHistory = SourceFile.getHistory();
        
        for (Item CurrentHistoryItem : FileHistory) {
            com.starbase.starteam.File CurrentHistoryFile = (File) CurrentHistoryItem;
            
            String FullFileName    = RootFolder + "/" + FormatOLEDATEToString(CurrentHistoryFile.getModifiedTime()) + "/Files" + SourceFolderName + CurrentHistoryFile.getName();
            String FullPath        = RootFolder + "/" + FormatOLEDATEToString(CurrentHistoryFile.getModifiedTime()) + "/Files" + SourceFolderName;
            String HistoryFileName = RootFolder + "/" + FormatOLEDATEToString(CurrentHistoryFile.getModifiedTime()) +"/@History.txt";
            
            System.out.format("FileName = %s; Revision = %d; CreatedTime = %s; Author = %s; Comment = '%s';%n",
                    FullFileName, CurrentHistoryFile.getRevisionNumber() + 1,
                    FormatOLEDATEToString(CurrentHistoryFile.getModifiedTime()),
                    FindAuthorNameById(CurrentHistoryFile.getModifiedBy()), CurrentHistoryFile.getComment());
            
            FileOutputStream fop = null;
	    java.io.File file;
            
            try {
                java.io.File directory = new java.io.File(FullPath);
                if (!directory.exists()) {
                    directory.mkdirs();
                }

                file = new java.io.File(FullFileName);
                fop = new FileOutputStream(file);
                
                CurrentHistoryFile.checkoutToStream(fop, com.starbase.starteam.Item.LockType.UNCHANGED, false);

                fop.flush();
                fop.close();
                
                java.io.File HistoryFile = new java.io.File(HistoryFileName);
                if (!HistoryFile.exists()) {
                    PrintWriter out = new PrintWriter(HistoryFileName);
                    out.println("AuthorID: " + CurrentHistoryFile.getModifiedBy());
                    out.println("AuthorName: " + FindAuthorNameById(CurrentHistoryFile.getModifiedBy()));
                    out.println("TimeStamp: " + FormatOLEDATEToString(CurrentHistoryFile.getModifiedTime()));
                    out.println("Comment: " + CurrentHistoryFile.getComment());
                    out.close();
                }

                //System.out.println("Done");
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fop != null) {
                        fop.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static void ExtractFullTreeFromRoot(com.starbase.starteam.Folder SourceFolder, String SourceFolderName, String RootFolder) {
        ExtractFilesFromFolder(SourceFolder, SourceFolderName, RootFolder);
        
        Item[] RootFolders = SourceFolder.getItems("Folder");
        
        for (Item CurrentItem : RootFolders) {
            Folder CurrentFolder = (Folder)CurrentItem;            
            ExtractFullTreeFromRoot(CurrentFolder, SourceFolderName+CurrentFolder.getPathFragment()+"/", RootFolder);
        }
    }

    private static void ExtractFilesFromFolder(com.starbase.starteam.Folder SourceFolder, String SourceFolderName, String RootFolder) {
        Item[] RootFiles = SourceFolder.getItems("File");
   
        for (Item CurrentItem : RootFiles) {
            com.starbase.starteam.File MyFile = (File) CurrentItem;
            if (SourceFolderName.isEmpty()) {
                ExtractFileHistory(MyFile, "/", RootFolder);
            } else {
                ExtractFileHistory(MyFile, SourceFolderName, RootFolder);
            }
        }
    }
    
    private static String FormatOLEDATEToString(OLEDate SourceValue) {
        DateFormat formatter = new SimpleDateFormat("yyyy.MM.dd.HH.mm");
        return formatter.format(SourceValue.createDate());
    }

Даже вполне не дурно получилось, на мой взгляд.

По такой схеме извлечения у меня получилось почти 2000 ревизий, что мало для столь долго живущего проекта, но это связано с принятыми здесь особенностями разработки и самим ПО. А также я долго проверял вручную действительно ли ревизии которые я извлёк правильные и правильно разложились.

Теперь осталось всё корректно закоммитить в SVN, а это уже чуть проще, чем первый этап.

Заливаем в SVN

Схема заливки в SVN простая:
1) Получаем полный список папок и сортируем его (именование папок нам в этом помогает);
2) Создаём список отсутствующих файлов и папок в папке SVN для текущей ревизии;
3) Копируем содержимое подпапки Files в папку SVN;
4) Для отсутствующих файлов и папок в SVN выполняем add;
5) Делаем коммит текущей рабочей копии SVN.

В ходе реализации я столкнулся с рядом проблем. А именно в комментариях к коммиту нужно перед двойными кавычками обязательно поставить обратный слэш, чтобы кавычки корректно обработались. И при добавлении файла или пути содержащего символ @ нужно в конце обязательно дописать ещё одну @, для того чтобы svn корректно понял имя файла. Тут код получился похуже, потому что хотелось побыстрее.

Заголовок спойлера

 //1. Получить список корневых директорий-ревизий
            java.io.File dir = new java.io.File("C:/StarTeamToSVN");

            java.io.File[] subDirs = dir.listFiles(new FileFilter() {
                @Override
                public boolean accept(java.io.File pathname) {
                    return pathname.isDirectory();
                }
            });
            
            //2. Отсортировать список
            Arrays.sort(subDirs);
            
            //3. Пройти по списку директорий и перебросить их содержимое
                //1. Получить список файлов с полными путями, которые собираюсь копировать
                //2. Создать список файлов которых нет в конечной папке SVN
                //3. Содержимое папки Files скопировать с заменой со всеми подпапками из StarTeam в SVN
                //4. Все файлы которые не добавлены в SVN добавить
                //5. Закоммитить ревизию с указанием автора и комментариями
            
            java.io.File RootSVN = new java.io.File("C:\TestASU");
            
            for (java.io.File CurrentDir : subDirs){
                try {
                    ArrayList<java.io.File> MyFiles = new ArrayList<>();
                    
                    String StarTeamSourceFolder = CurrentDir.getAbsolutePath()+"\Files\";
                    listf(StarTeamSourceFolder, MyFiles);
                    
                    ///String[] SVNAddFiles = new String[]();
                    ArrayList<String> SVNAddFiles = new ArrayList<>();
                    
                    for (java.io.File CurrentFile: MyFiles) {
                        String FullSourcePath = CurrentFile.getAbsolutePath();
                        String FullDestPath = "C:\TestASU\" + FullSourcePath.substring(FullSourcePath.indexOf(StarTeamSourceFolder) + StarTeamSourceFolder.length()) ;
                        
                        //сначала проверям все папки, а потом проверяем файлы
                        
                        java.io.File DestFile = new java.io.File(FullDestPath);
                        java.io.File ParentFolder = DestFile.getParentFile();
                        while ((ParentFolder != null) && (ParentFolder.compareTo(RootSVN) != 0 )) {
                            if (!ParentFolder.exists()) {
                                SVNAddFiles.add(0, ParentFolder.getAbsolutePath());
                            }
                            ParentFolder = ParentFolder.getParentFile();
                        }

                        if (!DestFile.exists()) {
                            SVNAddFiles.add(FullDestPath);
                        }
                    }
                    
                    //здесь сохранена версия файлов без всяких повторов
                    Set<String> s = new LinkedHashSet<>(SVNAddFiles);
                    
                    //копирование файлов
                    java.io.File RootStarTeam = new java.io.File(StarTeamSourceFolder);
                    try {
                        copyFolder(RootStarTeam, RootSVN);
                    } catch (IOException ex) {
                        Logger.getLogger(StarTeamToSVN.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    
                    Thread.sleep(5000);
                    
                    //добавить отсуствующие папки/файлы
                    for (String NewItem: s) {
                        SVNAddFile(NewItem, CurrentDir.getName());                 
                    }

                    //закоммитить с параметрами пользователями
                    String HistoryPath = CurrentDir.getAbsolutePath() + "\@History.txt";
                    String AuthorUserName = "";
                    String AuthorPassword = "";
                    String AuthorComment = "";
                    try {
                        for (String line : Files.readAllLines(Paths.get(HistoryPath), Charset.defaultCharset())) {
                            if (line.contains("AuthorID")) {
                                String AuthorID = line.substring(line.indexOf(": ")+2);
                                
                                AuthorUserName = FindAuthorUserNameByID(Integer.parseInt(AuthorID));
                                AuthorPassword = FindAuthorPasswordNameByID(Integer.parseInt(AuthorID));
                            }
                            
                            if (line.contains("TimeStamp")) {
                                AuthorComment = line.substring(line.indexOf(": ")+2);
                            }
                            
                            if (line.contains("Comment")) {
                                AuthorComment = AuthorComment + "n" + line.substring(line.indexOf(": ")+2);
                            }
                            
                            if (!line.contains(": ")) {
                                AuthorComment = AuthorComment + "n" + line;
                            }
                        }
                    } catch (IOException ex) {
                        Logger.getLogger(StarTeamToSVN.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    
                    if ((AuthorUserName == "")||(AuthorPassword == "")) {
                        throw new IOException("Ho Authentification data");
                    }
                    
                    Thread.sleep(1000);
                    
                    SVNCommit("C:\TestASU\", CurrentDir.getName(), AuthorUserName, AuthorPassword, AuthorComment);
                    
                } catch (InterruptedException ex) {
                    Logger.getLogger(StarTeamToSVN.class.getName()).log(Level.SEVERE, null, ex);
                }
            }

Тут есть временные задержки, потому что svn не всегда успевает всё дописать к себе в базу, а также файлы иногда интересны атнтивирусу, методом подбора получилось что при таких значениях проблем не возникает, а ещё у меня просто медленный хард, может поэтому тоже есть проблемы. В svn если выполнить svn add FOLDERNAME, то будет добавлена вся папка с содержимым, но мне показалось более верным способом добавлять поштучно папки и файлы, чтобы точнее контролировать процесс.

Заключение

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

А ещё я получил опыт программирования на Java. Код надо дорабатывать под конкретный проект, но он полностью работает и требует минимум доработок. Еще он позволит мигрировать тем, у кого разные версии SDK, или подогнать мой код под конкретную версию SDK.

Надеюсь, что кому-то эта статья будет полезна в будущем.

Автор: arch1tect0r

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js