Міграція проекту з StarTeam в 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) Копіюємо вміст папки Файли в папку 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.

Сподіваюся, що кому-то ця стаття буде корисна в майбутньому.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.