Skip to main content

„Best Practices“

Wiederherstellen eines Git-Projekts im Fall eines Crashes von CODESYS während der Projektbearbeitung

Je nachdem, bei welchem Vorgang CODESYS zuvor abgestürzt ist, haben Sie folgende Möglichkeiten zur Wiederherstellung:

  • Normalerweise können Sie nach einem Crash das Projekt erneut öffnen. CODESYS Git stellt den zuletzt intern gespeicherten Projektstand wieder her und schreibt die Projektinhalte erneut in die Git-Projektablage. Dieser "failsave"-Mechanismus behandelt das Projekt als "Master"-Datenquelle. Wenn das Projekt während eines Merge-Vorgangs abgestürzt ist, sollten Sie vor dem Weiterarbeiten nach dem erneuten Öffnen nicht gleich speichern, sondern zunächst alle Änderungen verwerfen (git reset --hard). Dazu können Sie in der Ansicht Status & Vormerken den Befehl Alle Änderungen verwerfen verwenden.

  • Falls das Öffnen des Projekts nach dem Crash nicht mehr gelingt, beispielsweise weil völlig inkompatible Dateien in die Projektablage gelangt waren, haben Sie folgende Möglichkeit einer Wiederherstellung: Laden Sie das Projekt komplett neu aus dem Repository. Sehen Sie dazu: Projekt aus Repository neu laden

Übertragen eines Projekts von CODESYS SVN nach CODESYS Git

Wir empfehlen Ihnen dringend, die SVN-Projektarchive nach der erfolgreichen Übertragung der Projekte nach GIT nicht zu löschen, um gegebenenfalls auf sie zurückgreifen zu können.

Tipp

Ab CODESYS Git Version 1.4.0.0 wird für die Übertragung eines Projekts von CODESYS SVN nach CODESYS Git eine Scripting-Schnittstelle zur Verfügung stehen.

Wichtig

Der Git-Befehl git-svn kann nicht für die Übertragung von CODESYS-Projekten aus CODESYS SVN nach CODESYS Git verwendet werden, da CODESYS Git im Gegensatz zum binären Dateiformat von CODESYS SVN das Dateiformat .json verwendet.

. Anmerkungen und Empfehlungen aufgrund der unterschiedlichen Projektarchiv-Struktur von CODESYS SVN und CODESYS Git
  • In CODESYS Git kann ein Git-Repository nur ein CODESYS-Projekt verwalten. Da ein SVN-Projektarchiv mehrere CODESYS-Projekte enthalten kann, muss bei der Übertragung nach CODESYS Git für jedes Projekt eines SVN-Projektarchivs ein eigenes Git-Repository erstellt werden.

  • Es wird empfohlen die einzelnen Projekte eines SVN-Projektarchivs projektweise in die Projektarchive von CODESYS Git zu übertragen.

  • Es wird nicht empfohlen ein SVN-Projektarchiv in mehrere SVN-Projektarchive aufzusplitten, da dies nachträglich zu Problemen führen kann.

  • Branches und Tags gibt es zwar sowohl in SVN als auch in GIT, allerdings sind sie in diesen beiden Systemen unterschiedlich konzipiert. Ein Transfer mit dem Befehl git-svn ist nicht möglich, da dieser Befehl in CODESYS Git nicht verwendet werden kann (siehe oben).

    Daher beschränken wir uns darauf, den SVN-Trunk in einen Git-Branch (standardmäßig "Master" oder "Main") zu übertragen. Wenn nötig, können die SVN-Tags manuell als Git-Tags hinzugefügt werden.

Bei diesem Verfahren wird zunächst die neueste Version des CODESYS-Projekts in CODESYS SVN ausgecheckt. anschließend wird die Verbindung zum SVN getrennt und für das Projekt ein Git-Repository mit CODESYS Git initialisiert. Der Nachteil dieses Verfahrens ist, dass die Historie des Projekts verloren geht, da nur die aktuellste SVN-Revision nach Git übertragen wird.

Voraussetzungen: CODESYS SVN und CODESYS Git sind in CODESYS installiert. Über den Befehl ToolsAnpassen wurde der Befehl Alles committen der Befehlskategorie Git Integration dem Menü Git hinzugefügt.

  1. Öffnen Sie das CODESYS-Projekt, das Sie in einem SVN-Projektarchiv gespeichert haben.

  2. Wählen Sie den Befehl ProjektSVNAuschecken.

    Die aktuellste Revision des Projekts ist ausgecheckt.

  3. Wählen Sie den Befehl ProjektSVNProjekt von SVN trennen.

  4. Erstellen Sie im lokalen Dateiverzeichnis Ihres Rechners einen neuen leeren Ordner.

  5. Wählen in CODESYS den Befehl GiTGit-Repostory installieren und wählen Sie in dem Dialog den in vorigen Schritt erstellten leeren Ordner aus.

  6. Wählen Sie den Befehl GiTAlles committen.

    Das Projekt ist jetzt im lokalen Git-Repository gespeichert und kann bei Bedarf in ein Remote-Repository gepusht werden.

Bei diesem Verfahren wird jede einzelne SVN-Revision in einen Git-Commit manuell übertragen.

Voraussetzungen: CODESYS SVN und CODESYS Git sind in CODESYS installiert. Über den Befehl ToolsAnpassen wurde der Befehl Alles committen der Befehlskategorie Git Integration dem Menü Git hinzugefügt

  1. Erstellen Sie in CODESYS ein neues leeres Projekt und geben Sie ihm beispielsweise den Namen Main Project

  2. Wählen Sie den Befehl GitGit-Repository initialisieren.

  3. Wählen Sie den Befehl GitAlles committen.

  4. Führen Sie einen leeren Commit aus, indem Sie den Befehl wählen GitCommit und dabei im Dialog Vorgemerkte Änderungen committen die Option Leeren Commit erlauben aktivieren.

  5. Schließen Sie dieses Projekt .

  6. Checken Sie die gewünschte Revision des in SVN abgelegten Projekts mit dem Befehl ProjektSVNAuschecken aus.

  7. Wählen Sie den Befehl ProjektSVNProjekt von SVN trennen und bestätigen Sie den anschließenden Dialog mit OK, ohne die standardmäßig eingestellte Option zu ändern.

  8. Speichern Sie das Projekt.

  9. Wählen Sie den Befehl GitGit-Repository initialisieren und wählen Sie dafür im Dateiverzeichnis einen leeren Ordner, beispielsweise Temp

  10. Wählen Sie den Befehl GitAlles committen.

  11. Speichern Sie das Projekt und schließen Sie es.

    Die Datei *.project dieses Projekts kann gelöscht werden, das Git-Repository Temp des Projekts darf auf keinen Fall gelöscht werden.

  12. Öffnen Sie erneut das Projekt Main Project.

  13. Öffnen Sie die Ansicht Remotes über den Befehl GitRemotes.

  14. Wählen Sie in der Ansicht Remotes den Befehl Add und tragen Sie im Dialog Neuen Remote hinzufügen die URL des Git-Repositorys Temp ein und als Alias-Name zum Beispiel Temp_Remote ein.

  15. Selektieren Sie diesen hinzugefügten Remote und wählen Sie den Befehl Fetch.

  16. Öffnen Sie die Ansicht Git-Branches über den Befehl GitBranches.

  17. Selektieren Sie den Master/Main Branch von Main, wählen Sie den Befehl Branch tracken und wählen Sie im Dialog Remote-Branch tracken den Master/Main-Branch des in Schritt 14 hinzugefügten Remotes Temp_Remote.

  18. Führen Sie einen Pull auf den Master/Main-Branch des Git-Repositorys Main aus indem Sie in der Ansicht Git-Branches den Befehl Pull mit Optionen wählen und im Dialog Git pull master als Merge-Konflikt-Strategie die Option "Ihres" für Konflikte verwenden auswählen. Die voreingestellte Option für Fast-Forward-Strategie sollte nicht geändert werden.

  19. Wählen Sie nun den Befehl Git -→ Alles committen und wählen Sie dabei im Dialog Vorgemerkte und nicht vorgemerkte Änderungen committen die Option Commit abändern.

    Die zu Beginn der Anleitung ausgecheckte Revision des SVN-Projekts ist jetzt im Git-Repository Main als Commit abgelegt.

  20. Selektieren Sie in der Ansicht Remotes den Remote Temp und wählen Sie den Befehl Entfernen.

  21. Speichern Sie das Projekt Main Project und schließen Sie es.

  22. Entfernen Sie das Git-Repository Temp aus Ihrem Dateiverzeichnis.

  23. Wenn Sie noch eine weitere Revision des SVN-Projekts in das Git-Repository übertragen wollen, dann führen Sie diese Anleitung ab Schritt 6 für die nächste gewünschte Revision des SVN-Projekts durch.

Übertragung SVN-Projekts nach Git mit Scripting

Voraussetzung: CODESYS Git Version >= 1.4.0.0

Folgende Skriptvorlagen für die Übertragung werden im Folgenden angezeigt:

1. Skript für die Übertragung eines Projekts von CODESYS SVN nach CODESYS Git

2. Skript für die Überprüfung, ob bei der Übertragung für jede SVN-Revision ein entsprechender Git-Commit erstellt wurde

. Hinweise
  • Am SVN-Repository werden keinerlei Änderungen vorgenommen.

  • Beide Skripte sind als Vorlage gedacht, die Sie an die jeweiligen Anforderungen anpassen müssen.

Skript für die Übertragung eines SVN-Projekts

Das Skript überträgt alle SVN-Revisionen des Trunks eines CODESYS SVN-Projekts in den "Master"-Branch eines CODESYS Git-Projekts.

import os
import shutil
import logging


""" --- inputs ---
"""
# URL of the SVN project you want to migrate
# only migrate a single project at a time if you have multiple in your SVN repo
SVN_REPO_URL = 'svn://your.svnserver.com/trunk/CodesysProjectNr1'
# Starts migration at this svn revision
STARTING_REVISION = 0
# Directory of the new project with the git repository
NEW_PROJECT_DIR = 'C:/CodesysProjects/CodesysProjectNr1'
# Name of the new project
NEW_PROJECT_NAME = 'CodesysProjectNr1'
# Is the project a library (otherwise will be checked out as a project)
IS_LIBRARY = False
# Path to the local git repository of the new project
NEW_PROJECT_REPO_DIR = 'C:/CodesysProjects/CodesysProjectNr1/repo'

# Path to an EMPTY temporary folder (doesn't have to exist) that can be deleted
TEMP_PATH = 'C:/CodesysProjects/CodesysProjectNr1/temp'

# Author of the git commits
AUTHOR_NAME = 'Author Name'
# E-mail address of the git commits
AUTHOR_EMAIL = 'Author.Name@e-mail.com'

# Name of the branch that should contain the history
# Can't be named 'master'
TARGET_BRANCH_NAME = 'develop'
# Initial commit messages on the master branch and the target branch
IC_MSG_MASTER = 'master: Initial commit.'
IC_MSG_TARGET_BRANCH = 'develop: Initial commit.'

# Removes the newly created project and its repository if the migration fails
REMOVE_MAIN_PROJECT_ON_FAILURE = True


def get_git_commit_msg(svn_log):
    """ Returns the commit messages that will be seen in the resulting git history
        Can be customized to ones demands.
        *svn_log* see: https://content.helpme-codesys.com/en/ScriptingEngine/ScriptSubversion.html#ScriptSubversion.LogInfo
    """
    return 'SVN revision %d: %s' % (svn_log.revision, svn_log.message)


""" --- migration script ---
"""
# Type extension of the project (.project / .library)
PROJECT_FILE_TYPE_EXTENSION = '.library' if IS_LIBRARY else '.project'
# Path to your new Git project
NEW_MAIN_PROJECT_PATH = os.path.join(NEW_PROJECT_DIR, NEW_PROJECT_NAME + PROJECT_FILE_TYPE_EXTENSION)
# Name of the temporarily added remote
TEMP_REMOTE_NAME = 'remote1'
# Temporary commit message will be overwritten/amended (put what u like)
TEMP_COMMIT_MSG = 'TEMP COMMIT: This will be overwritten'


def create_git_project():
    """ 1. Creates a new project at *NEW_MAIN_PROJECT_PATH*
        2. Initializes it in git with the repository at *NEW_PROJECT_REPO_DIR*
        3. Makes an initial commit with the message *IC_MSG_MASTER*
        4. Creates and switches to the target branch *TARGET_BRANCH_NAME*
        5. Makes an initial commit on the target branch with the message *IC_MSG_TARGET_BRANCH*
        6. Saves and closes the project
    """
    git_project = projects.create(NEW_MAIN_PROJECT_PATH)
    git_project.git.init(NEW_PROJECT_REPO_DIR)
    git_project.git.commit_complete(IC_MSG_MASTER, AUTHOR_NAME, AUTHOR_EMAIL)

    git_project, git_branch = git_project.git.branch_copy(TARGET_BRANCH_NAME, checkout=True)
    git_project.git.commit_complete(IC_MSG_TARGET_BRANCH, AUTHOR_NAME, AUTHOR_EMAIL, bAllowEmptyCommits=True)

    git_project.save()
    git_project.close()


def remove_all_children(project):
    """ Removes all objects that can be removed from the project *project*
    """
    children = project.get_children(recursive=True)
    for child in children:
        try:
            child.remove()
        except Exception as ex:
            if not 'Object reference not set to an instance of an object.' == str(ex):
                logging.error(ex)


def create_git_repo_from_svn_revision(svn_log):
    """ 1. Checks out a project from the SVN server *SVN_REPO_URL*
        2. Disconnects it from SVN
        3. Initiates it in git with the repository at *repo_dir*
        4. Makes a commit with the message *commit_message*
            4.1 See "get_git_commit_msg" to customize the commit message
        5. Saves and closes the project
        6. Returns the path of the project and path of the repository
    """
    project_path = os.path.join(TEMP_PATH, 'revision_' + str(svn_log.revision) + PROJECT_FILE_TYPE_EXTENSION)
    repo_dir = os.path.join(TEMP_PATH, 'repo_' + str(svn_log.revision))
    commit_message = get_git_commit_msg(svn_log)

    temp_project_name = 'revision_%d' % svn_log.revision
    svn_project = svn.checkout(SVN_REPO_URL, TEMP_PATH, temp_project_name, svn_log.revision, as_library=IS_LIBRARY)

    svn_project.svn.disconnect()
    svn_project.git.init(repo_dir)
    svn_project.git.commit_complete(commit_message, AUTHOR_NAME, AUTHOR_EMAIL)
    svn_project.save()
    svn_project.close()

    return project_path, repo_dir


def magic_git_pull(project, temp_repo_path, svn_log):
    """ 1. Removes everything from the *project* (to insure svn revision and git commit match exactly)
        2. Pulls from the repo at *repo_path*
        4. Commits the changes with the commit message *commit_message*
        5. Saves and closes the project
    """
    remove_all_children(project)

    # Temp commit for fake git merge --squash
    project.git.commit_complete(TEMP_COMMIT_MSG, AUTHOR_NAME, AUTHOR_EMAIL, bAllowEmptyCommits=True)

    project.git.remote_add(TEMP_REMOTE_NAME, temp_repo_path)
    project.git.fetch(TEMP_REMOTE_NAME)
    project.git.branch_track('refs/remotes/%s/master' % TEMP_REMOTE_NAME, TARGET_BRANCH_NAME)

    pull_options = git.get_pull_options()
    pull_options.fast_forward_strategy = GitFastForwardStrategy.NoFastForward
    pull_options.resolve_file_conflict_strategy = GitResolveFileConflictStrategy.Theirs
    project, merge_result = project.git.pull(AUTHOR_NAME, AUTHOR_EMAIL, pull_options)

    commit_message = get_git_commit_msg(svn_log)
    project.git.commit_complete(commit_message, AUTHOR_NAME, AUTHOR_EMAIL, bAllowEmptyCommits=True, bAmendCommit=True)

    project.git.branch_unset_upstream()
    project.git.remote_remove(TEMP_REMOTE_NAME)

    project.save()
    project.close()


def remove_repo(project_path):
    try:
        project = projects.open(project_path)
        project.git.de_init(True)
        project.close()
    except:
        pass


def remove_temp_project(project_path):
    """ Opens the project at *project_path* and deinitializes its git repository
        This needs to be done because the script can't delete the repository with "shutil.rmtree"
        Removes the rest of the contents int the folder at *TEMP_PATH*
        Removes the temporary .project/.library files and their associated files
    """
    remove_repo(project_path)
    try:
        shutil.rmtree(TEMP_PATH)
        os.makedirs(TEMP_PATH)
    except Exception as e:
        logging.error('Failed to delete %s. Reason: %s' % (TEMP_PATH, e))


def main():
    if projects.primary is not None:
        projects.primary.close()

    create_git_project()

    # get the list of all revision of SVN_REPO_URL
    # oldest to newest
    # starting at revision STARTING_REVISION + 1
    svn_logs = list(reversed(svn.get_log(SVN_REPO_URL)))
    svn_logs_to_migrate = [log for log in svn_logs if STARTING_REVISION <= log.revision]
    assert svn_logs_to_migrate, ('Nothing to migrate. STARTING_REVISION is greater than the newest '
                                 'revision in %s') % SVN_REPO_URL

    system.prompt_answers['LossOfDataWarning2'] = PromptResult.Yes
    no_project_root_dir_amount = 0
    try:
        for svn_log in svn_logs_to_migrate:
            try:
                temp_project_path, temp_git_repo_path = create_git_repo_from_svn_revision(svn_log)

                # pull to your main Git project
                git_project = projects.open(NEW_MAIN_PROJECT_PATH)

                magic_git_pull(git_project, temp_git_repo_path, svn_log)

                # can be omitted if enough storage space is available (big performance increase)
                # if omitted the folder at "TEMP_PATH" needs to be deleted manually
                # needed storage space = (project file size + project git repository size) * revisions
                # example:   255.16 MB = (1.6 KB + 2.55 MB) * 100
                remove_temp_project(temp_project_path)
            except ValueError as ve:
                # Early svn revisions often do not contain a codesys project
                if ('The URL %s is not a project root directory.' % SVN_REPO_URL) == str(ve):
                    no_project_root_dir_amount += 1
                    if no_project_root_dir_amount < len(svn_logs_to_migrate):
                        logging.info(ve)
                        continue
                logging.critical(ve)
                raise
    except:
        if REMOVE_MAIN_PROJECT_ON_FAILURE:
            remove_repo(NEW_MAIN_PROJECT_PATH)
            for file in os.listdir(NEW_PROJECT_DIR):
                if file.startswith(NEW_PROJECT_NAME):
                    os.remove(os.path.join(NEW_PROJECT_DIR, file))
        raise
    finally:
        system.prompt_answers.clear()


if __name__ == '__main__':
    main()

Skript für die Überprüfung des Übertragungsergebnisses

Das unten gezeigte Skript dient folgenden Überprüfungen:

  1. Überprüfung, ob es nach der Übertragung für jede SVN-Revision des Trunks eines CODESYS SVN-Projekts einen entsprechenden Git-Commit im CODESYS Git-Projekt gibt

  2. Überprüfung, ob das CODESYS-Projekt einer SVN-Revision und das CODESYS-Projekt des entsprechenden Git-Commits identisch sind

Der Vergleich eines CODESYS-Projekts basiert auf der CODESYS-Entwicklungsumgebung, in der die Übertragung und die Überprüfung durchgeführt werden.

Anmerkung

Die in CODESYS übliche Skripting-Methode compare_to() verhält sich in diesem Zusammenhang nicht wie erwartet. Aus diesem Grund wird in dem Script ein Workaround verwendet.

import os
import logging


""" --- inputs ---
"""
# URL of the SVN project you want to check the migration for
SVN_REPO_URL = 'svn://your.svnserver.com/trunk/CodesysProjectNr1'
# Directory of the project with git repository to check
PROJECT_DIR = 'C:/CodesysProjects/CodesysProjectNr1'
# Name of the project to check
PROJECT_NAME = 'CodesysProjectNr1'
# Is the project a library (otherwise will be checked out as a project)
IS_LIBRARY = False

# Path to an EMPTY temporary folder (doesn't have to exist) that can be deleted
TEMP_PATH = 'C:/CodesysProjects/CodesysProjectNr1/temp'

# Name of the branch that should be checked
TARGET_BRANCH_NAME = 'develop'

""" --- check script ---
"""
# Type extension of the project (.project / .library)
PROJECT_FILE_TYPE_EXTENSION = '.library' if IS_LIBRARY else '.project'
# Path to your new Git project
MAIN_PROJECT_PATH = os.path.join(PROJECT_DIR, PROJECT_NAME + PROJECT_FILE_TYPE_EXTENSION)


def get_revision_from_commit_msg(msg):
    """ filters out the revision number from the commit message *msg*
        migrated SVN commits in git: git commit = 'SVN revision *revision number*: *svn commit message*'
        if your migrated svn commits in git are different this possibly won't work
    """
    s_num = ''
    for c in msg:
        if c.isdigit():
            s_num = s_num + c
        if ':' == c:
            break
    return int(s_num)


def compare_changed_objects_workaround(changed_objects):
    """ Compares the changed objects in the *changed_objects* list
        by comparing their content
        Returns True if the objects are identical (not changed)
    """
    for changed_object in changed_objects:
        if changed_object.left_object is None:
            return False
        if changed_object.right_object is None:
            return False
        if not changed_object.left_object.textual_declaration.text == changed_object.right_object.textual_declaration.text:
            logging.error('Compared objects are not the same:')
            logging.error('left: %s/%s' %
                          (changed_object.left_object.parent.get_name(), changed_object.left_object.get_name()))
            logging.error(changed_object.left_object.textual_declaration.text)
            logging.error('right: %s/%s' %
                          (changed_object.right_object.parent.get_name(), changed_object.right_object.get_name()))
            logging.error(changed_object.right_object.textual_declaration.text)
            logging.error('diff: ' + str(changed_object.differences))
            logging.error('-----------------------------------------------------')
            return False
    return True


def compare_projects(project_one, project_two):
    """ Compares two projects
        Returns True if they are identical
    """
    comparison_result = project_one.compare_to(project_two, flags=ComparisonFlags.IGNORE_PROPERTIES)
    changed_objects = list(comparison_result.get_changed_objects())

    if 0 == len(changed_objects):
        return True
    else:
        return compare_changed_objects_workaround(changed_objects)


def main():
    system.prompt_handling = PromptHandling.LogSimplePrompts

    if projects.primary is not None:
        projects.primary.close()

    git_project = projects.open(MAIN_PROJECT_PATH)

    git_project, git_branch = git_project.git.checkout(TARGET_BRANCH_NAME, force=True)

    git_logs = list(reversed(list(git_project.git.log())))

    system.prompt_answers['LossOfDataWarning2'] = PromptResult.Yes

    checks_done = 0
    try:
        for git_log in git_logs:
            if 'SVN revision ' in git_log.message_string:
                git_project, git_null_branch = git_project.git.checkout(git_log.sha_string)

                revision = get_revision_from_commit_msg(git_log.message_string)

                temp_project = svn.checkout(SVN_REPO_URL, TEMP_PATH, 'revision_' + str(revision), revision,
                                            as_library=IS_LIBRARY ,as_primary_project=False)

                if not compare_projects(git_project, temp_project):
                    prompt_message = ("Revision %d does not equal it's corresponding git commit. "
                                      "Do you want to continue?") % revision
                    logging.error(prompt_message)
                    prompt_result = system.ui.prompt(prompt_message, PromptChoice.YesNo, PromptResult.No)
                    if PromptResult.No == prompt_result:
                        temp_project.close()
                        break

                checks_done += 1
                temp_project.close()
    finally:
        system.prompt_answers.clear()
        git_project, git_branch = git_project.git.checkout(TARGET_BRANCH_NAME)
        git_project.close()

    if checks_done:
        system.ui.prompt('Done', PromptChoice.OK, PromptResult.OK)
    else:
        error_msg = 'No migrated commits found. If you have custom commit messages change this script accordingly.'
        logging.error(error_msg)
        raise Exception(error_msg)


if __name__ == '__main__':
    main()