├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── History └── History_SampleComponent_Streamname.txt ├── LICENSE ├── README.md ├── addons ├── .directoryignore └── extension_hunter.py ├── config.ini.minimum.sample ├── config.ini.sample ├── configuration.py ├── gitFunctions.py ├── migration.py ├── rtcFunctions.py ├── shell.py ├── shouter.py ├── sorter.py └── tests ├── resources ├── test_.gitignore ├── test_.jazzignore ├── test_config.ini ├── test_ignore_git_status_z.txt ├── test_minimum_config.ini ├── test_rtcFunctions_SampleCompareOutputInUtf8.txt ├── test_rtcFunctions_SampleCompareOutputWithComponents.txt ├── test_rtcFunctions_SampleCompareOutputWithLineBreaks.txt └── test_rtcFunctions_SampleCompareOutputWithoutLineBreaks.txt ├── test_configuration.py ├── test_gitFunctions.py ├── test_migration.py ├── test_rtcFunctions.py ├── test_shell.py ├── test_sorter.py └── testhelper.py /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | 4 | # Explicitly declare text files you want to always be normalized and converted 5 | # to native line endings on checkout. 6 | *.py text 7 | *.md text 8 | *.ini text 9 | *.sample text -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.project 3 | config.ini 4 | Compare_*.txt 5 | StreamComponents_*.txt 6 | accept*.txt 7 | History_*.txt 8 | !History_SampleComponent_Streamname.txt 9 | __pycache__ 10 | *.swp 11 | *.pyc 12 | *~ 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.4" 4 | - "3.5" 5 | - "3.5-dev" # 3.5 development branch 6 | - "3.6" 7 | - "3.6-dev" # 3.6 development branch 8 | - "3.7-dev" # 3.7 development branch 9 | - "nightly" # currently points to 3.7-dev 10 | before_script: 11 | - git config --global user.email "manuel@rtc.to" 12 | - git config --global user.name "Manuel W" 13 | 14 | script: python -m unittest discover -s tests 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [Unreleased](https://github.com/rtcTo/rtc2git/tree/HEAD) 4 | 5 | [Full Changelog](https://github.com/rtcTo/rtc2git/compare/v1.1...HEAD) 6 | 7 | **Implemented enhancements:** 8 | 9 | - Check ignored files which still are present [\#72](https://github.com/rtcTo/rtc2git/issues/72) 10 | - Specify user/password on the command line [\#69](https://github.com/rtcTo/rtc2git/issues/69) 11 | - .jazzignore -\> .gitignore [\#66](https://github.com/rtcTo/rtc2git/issues/66) 12 | - Configuration of conflict resolver [\#55](https://github.com/rtcTo/rtc2git/issues/55) 13 | - Ability to use -i when loading a workspace [\#50](https://github.com/rtcTo/rtc2git/issues/50) 14 | - Link commits with Issue-System \(Jira/Github/BitBucket\) [\#48](https://github.com/rtcTo/rtc2git/issues/48) 15 | - Provide configuration with fallback-values [\#47](https://github.com/rtcTo/rtc2git/issues/47) 16 | - Decide if migration should be resumed or is started [\#42](https://github.com/rtcTo/rtc2git/issues/42) 17 | - Make migrated branch to master [\#41](https://github.com/rtcTo/rtc2git/issues/41) 18 | - Ignore binary files [\#33](https://github.com/rtcTo/rtc2git/issues/33) 19 | - Make encoding configurable [\#30](https://github.com/rtcTo/rtc2git/issues/30) 20 | - Add configuration to choose between lscm and scm [\#28](https://github.com/rtcTo/rtc2git/issues/28) 21 | - Ability to accept multiple \(more than two\) change sets together to avoid conflicts [\#27](https://github.com/rtcTo/rtc2git/issues/27) 22 | - Check Replacing of InitialBaseLines/OldestStream [\#23](https://github.com/rtcTo/rtc2git/issues/23) 23 | - One-way Bridge [\#11](https://github.com/rtcTo/rtc2git/issues/11) 24 | - CLI Support [\#10](https://github.com/rtcTo/rtc2git/issues/10) 25 | - Start migration from an existing prepared workspace vs newly created workspace [\#8](https://github.com/rtcTo/rtc2git/issues/8) 26 | - Start migration from a specific baseline [\#6](https://github.com/rtcTo/rtc2git/issues/6) 27 | - Accept changesby date instead of component [\#5](https://github.com/rtcTo/rtc2git/issues/5) 28 | - Loop through migrated branches and compare with stream [\#4](https://github.com/rtcTo/rtc2git/issues/4) 29 | 30 | **Fixed bugs:** 31 | 32 | - .gitignore files get lost [\#85](https://github.com/rtcTo/rtc2git/issues/85) 33 | - added files in .gitignore should be non-recursive [\#81](https://github.com/rtcTo/rtc2git/issues/81) 34 | - Travis build sometimes failing [\#77](https://github.com/rtcTo/rtc2git/issues/77) 35 | - output of git status -z should not be stripped [\#75](https://github.com/rtcTo/rtc2git/issues/75) 36 | - "\" comment is translated to "\ .gitignore [\#71](https://github.com/rtcTo/rtc2git/pull/71) ([ohumbel](https://github.com/ohumbel)) 83 | - enable RTC credentials on the command line [\#70](https://github.com/rtcTo/rtc2git/pull/70) ([ohumbel](https://github.com/ohumbel)) 84 | - Conflictresolution within the same component [\#64](https://github.com/rtcTo/rtc2git/pull/64) ([ohumbel](https://github.com/ohumbel)) 85 | - Fixed comparison [\#60](https://github.com/rtcTo/rtc2git/pull/60) ([jacobilsoe](https://github.com/jacobilsoe)) 86 | - List files with specific extensions in your workspace [\#59](https://github.com/rtcTo/rtc2git/pull/59) ([ohumbel](https://github.com/ohumbel)) 87 | - handle relative file names in tests [\#58](https://github.com/rtcTo/rtc2git/pull/58) ([ohumbel](https://github.com/ohumbel)) 88 | - Added configuration of change set accept max count [\#57](https://github.com/rtcTo/rtc2git/pull/57) ([jacobilsoe](https://github.com/jacobilsoe)) 89 | - Link commits by adding a prefix on comments [\#56](https://github.com/rtcTo/rtc2git/pull/56) ([WtfJoke](https://github.com/WtfJoke)) 90 | - Make inclusion of component roots configurable [\#54](https://github.com/rtcTo/rtc2git/pull/54) ([ohumbel](https://github.com/ohumbel)) 91 | - block invalid branch names [\#53](https://github.com/rtcTo/rtc2git/pull/53) ([ohumbel](https://github.com/ohumbel)) 92 | - the config file can be specified on the command line [\#46](https://github.com/rtcTo/rtc2git/pull/46) ([ohumbel](https://github.com/ohumbel)) 93 | - Allow ignoring big files \(by extension\) [\#45](https://github.com/rtcTo/rtc2git/pull/45) ([ohumbel](https://github.com/ohumbel)) 94 | - Quick fix for issue \#39 [\#40](https://github.com/rtcTo/rtc2git/pull/40) ([ohumbel](https://github.com/ohumbel)) 95 | - \[fix\] Add quotes around the workspace name [\#37](https://github.com/rtcTo/rtc2git/pull/37) ([cwill747](https://github.com/cwill747)) 96 | - Fix for issue 31 [\#34](https://github.com/rtcTo/rtc2git/pull/34) ([ljhaywar](https://github.com/ljhaywar)) 97 | - UTF-8 support, configuration of conflict resolution and configuration of scm command [\#26](https://github.com/rtcTo/rtc2git/pull/26) ([jacobilsoe](https://github.com/jacobilsoe)) 98 | - Fix Issue 14 [\#15](https://github.com/rtcTo/rtc2git/pull/15) ([WtfJoke](https://github.com/WtfJoke)) 99 | - Some suggestions [\#9](https://github.com/rtcTo/rtc2git/pull/9) ([ebullient](https://github.com/ebullient)) 100 | 101 | 102 | 103 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /History/History_SampleComponent_Streamname.txt: -------------------------------------------------------------------------------- 1 | _SOMERANDOMUUID 2 | _SOMEOTHERANDOMUUID 3 | _ANDSOONANOTHERUUID 4 | _OLDESTUUID -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Manuel Wessner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/rtcTo/rtc2git.svg)](https://travis-ci.org/rtcTo/rtc2git) 2 | [![Supported Versions](https://img.shields.io/badge/python-3.4%2C%203.5%2B-blue.svg)](https://travis-ci.org/rtcTo/rtc2git) 3 | [![MIT License](https://img.shields.io/badge/license-MIT-orange.svg)](https://github.com/rtcTo/rtc2git/blob/develop/LICENSE) 4 | 5 | # rtc2git 6 | 7 | A tool made for migrating code and code-history from an existing [RTC](https://jazz.net/products/rational-team-concert/) SCM repository into a Git repository 8 | It uses the CLI of RTC to gather the required informations. 9 | 10 | ## Prerequirements 11 | 12 | - RTC Server with Version 5.0+ (was tested using 5.0.1) 13 | - **[SCM Tools](https://jazz.net/downloads/rational-team-concert/releases/5.0.1?p=allDownloads)** from IBM. 14 | To avoid an account creation on the jazz.net site, you could use [bugmenot](http://bugmenot.com/). 15 | Please make sure that your SCM Tools run in **English** (because we need to parse their output sometimes). 16 | There is a wiki page on how to [configure RTC CLI](https://github.com/rtcTo/rtc2git/wiki/configure-RTC-CLI)) 17 | - Python 3.4+ (does not work with previous versions or with Python 2) 18 | 19 | ## Development-Status 20 | For migrating bigger repositories (> 10000 changes) I advise you to use our other tool [rtc2gitcli](https://github.com/rtcTo/rtc2gitcli) as the IBM Java API is more stable than IBM CLI API. 21 | However, this project is easier to run and adapt to different environments. 22 | 23 | This project is no longer in active development, because the author has no access to any RTC Server anymore (since they are migrated to git) and changes to code can only be hardly tested. 24 | 25 | ## Usage 26 | 27 | - Create a config file called `config.ini` and fill out the needed information, use the supplied `config.ini.sample` or `config.ini.minimum.sample` as reference 28 | - Execute `migration.py` 29 | 30 | 31 | ### Pitfalls 32 | - Your stream or workspace is not allowed to have spaces in their name. In the case where names contain spaces, please clone and rename them and use the cloned workspace/stream for migration (see [#104](https://github.com/rtcTo/rtc2git/issues/104), [#51](https://github.com/rtcTo/rtc2git/issues/51)). 33 | - Sometimes rtc2git can not determine any baseline and won't find any changes (accepting changesets 0) - Please refer to how to reset your workspace to an older state explained [here](https://github.com/rtcTo/rtc2git/wiki/Resetting-your-workspace-to-an-older-state) 34 | - The provided result of the compare command of IBM RTC CLI API does sometimes not provide the changesets in the fully correct order. This can result in merge conflicts, which should be solved by loading the workspace into eclipse, manually resolving them and resuming the migration by running the rtc2git again. 35 | 36 | ## How does it work? 37 | 38 | 1. It initalizes an empty git repository and clones it 39 | 2. In this repository, it loads a newly created (which will be set to the oldest baseline possible) or existing rtc workspace 40 | 3. The baseline of each component of a given stream is determined 41 | 4. For each baseline a compare command will be executed 42 | 5. The result of the compare will be parsed to get to the necessary commit-information (such as author, comment, date) 43 | 6. The change will be accepted in the workspace 44 | 7. The corresponding git command will be executed to do the same change in the git-repository 45 | 46 | 47 | ## Contribute 48 | 49 | We welcome any feedback! :) 50 | 51 | Feel free to report and/or fix [issues](https://github.com/rtcTo/rtc2git/issues) or create new pull requests 52 | 53 | ### Pull-Requests 54 | 55 | 1. [Fork it](https://github.com/rtcTo/rtc2git#fork-destination-box) 56 | 2. Create your feature branch (`git checkout -b my-new-feature`) 57 | 3. Commit your changes (`git commit -am 'Add some feature'`) 58 | 4. Push to the branch (`git push origin my-new-feature`) 59 | 5. Create new Pull Request 60 | 61 | ## Wiki 62 | 63 | For more details [visit our wiki](https://github.com/rtcTo/rtc2git/wiki) 64 | You could also join us in [slack](https://rtc.to/#slack). 65 | -------------------------------------------------------------------------------- /addons/.directoryignore: -------------------------------------------------------------------------------- 1 | .jazz5 2 | .jazzShed 3 | .git 4 | .metadata 5 | CVS 6 | 7 | -------------------------------------------------------------------------------- /addons/extension_hunter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | from os.path import join, getsize 4 | 5 | 6 | def parsecommandline(): 7 | parser = argparse.ArgumentParser() 8 | directoryhelp = 'the root directory to start with' 9 | parser.add_argument('-d', '--directory', metavar='directory', dest='directory', help=directoryhelp, required=True) 10 | extensionshelp = 'a list of file extensions, such as: .zip .jar .exe (you can omit the leading .)' 11 | parser.add_argument('-e', '--extensions', metavar='extension', dest='extensions', help=extensionshelp, nargs='+', required=True) 12 | ignorehelp = 'directories to ignore, such as: bin out (common directories are listed in .directoryignore)' 13 | parser.add_argument('-i', '--ignoredirectories', metavar='directory', dest='directoriestoignore', help=ignorehelp, nargs='+', default=[], required=False) 14 | arguments = parser.parse_args() 15 | extensions = arguments.extensions 16 | i = 0 17 | for extension in extensions: 18 | if extension[0] != '.': 19 | extensions[i] = '.' + extension 20 | i += 1 21 | return (arguments.directory, extensions, arguments.directoriestoignore) 22 | 23 | 24 | def read_directoryignore(): 25 | directoriestoignore = [] 26 | with open('./.directoryignore', 'r') as ignore: 27 | lines = ignore.readlines() 28 | for line in lines: 29 | directoriestoignore.append(line.strip()) 30 | return directoriestoignore 31 | 32 | 33 | if __name__ == "__main__": 34 | (directory, extensions, directoriestoignore) = parsecommandline() 35 | for defaultdirectory in read_directoryignore(): 36 | directoriestoignore.append(defaultdirectory) 37 | 38 | for dir, subdirs, files in os.walk(directory): 39 | for directorytoignore in directoriestoignore: 40 | if directorytoignore in subdirs: 41 | subdirs.remove(directorytoignore) 42 | for file in files: 43 | for extension in extensions: 44 | if len(file) >= len(extension): 45 | if file.endswith(extension): 46 | filename = join(dir, file) 47 | print('%10d %s' % (getsize(filename), filename)) 48 | -------------------------------------------------------------------------------- /config.ini.minimum.sample: -------------------------------------------------------------------------------- 1 | [General] 2 | Repo=https://rtc.mySite.com/ccm 3 | User=USR 4 | Password=secret 5 | GIT-Reponame = myGitRepo.git 6 | WorkspaceName=ToBeCreatedWorkspaceName 7 | # Folder to be created - where migration will take place 8 | Directory = \temp\myWorkingDirectory 9 | 10 | [Migration] 11 | # Stream to be migrated, referenced by Name or UUID 12 | StreamToMigrate = MyDevelopmentStream 13 | -------------------------------------------------------------------------------- /config.ini.sample: -------------------------------------------------------------------------------- 1 | [General] 2 | Repo=https://rtc.mySite.com/ccm 3 | User=USR 4 | Password=secret 5 | GIT-Reponame = myGitRepo.git 6 | WorkspaceName=ToBeCreatedWorkspaceName 7 | # If this value is set to True, the workspace referred with workspacename will just be loaded instead of newly created 8 | useExistingWorkspace = False 9 | # Folder to be created - where migration will take place 10 | Directory = \temp\myWorkingDirectory 11 | # Scm command to use (lscm or scm) 12 | ScmCommand = lscm 13 | # Optional - Set encoding of files (For example encoding = UTF-8) 14 | # See "https://github.com/rtcTo/rtc2git/wiki/Encoding" for further instructions 15 | encoding = 16 | # Optional - SCM version. Different SCM versions have different commands. If not specific, default is 5. 17 | RTCVersion = 5 18 | 19 | [Migration] 20 | # Stream to be migrated, referenced by Name or UUID 21 | StreamToMigrate = MyDevelopmentStream 22 | 23 | # Optional - Used by author for prepare a workspace for migrating an earlier release-stream to a later one 24 | # (eg. When Migrating Stream_Version2, Previous-Stream would be Stream_Version1 25 | PreviousStream= 26 | 27 | # Optional, can be defined additionally to set the workspace to a specific baseline 28 | # Use following format: ComponentName = BaseLineName, AnotherComponentName=BaseLineName 29 | # If its not set, it will determine the oldest baseline (takes some time, depending of how much components you have in your stream) 30 | InitialBaseLines = 31 | 32 | # False - Rely on order of changeset provided by the rtc cli compare command (due wrong order, more likely to cause merge-conflicts 33 | # True - (Component)History needs to be provided in a separate file by the user 34 | # For more information read https://github.com/rtcTo/rtc2git/wiki/Getting-your-History-Files 35 | UseProvidedHistory = False 36 | 37 | # Determines whether to prompt the user to accept change sets together to resolve accept errors. 38 | # False - The user is prompted 39 | # True - The user is not prompted 40 | UseAutomaticConflictResolution = False 41 | 42 | # The max number of change sets to accept together in order to resolve a merge conflict. 43 | MaxChangeSetsToAcceptTogether = 10 44 | 45 | # Optional, defines a prefix for commit-messages, which were linked to an rtc workitem. 46 | # In case you have migrated your workitems to another issue system (like jira/github/bitbucket) by using rtc2jira, 47 | # you can define a prefix for the commit-message, in order that previously linked rtc commits get linked to the new issue system. 48 | # 49 | # Examples: 50 | # In jira: AP- (Project has the key AP and is followed by a dash) 51 | # see https://confluence.atlassian.com/jiracloud/processing-jira-issues-with-commit-messages-740098538.html for more details) 52 | # On bitbucket/github: # 53 | CommitMessageWorkItemPrefix= 54 | 55 | # Optional: Specifies the line(s) which are added to .gitattributes 56 | # Define a semicolon-separated list of lines 57 | # Example: 58 | # Gitattributes = # handle text files; * text=auto; *.sql text 59 | Gitattributes = 60 | 61 | [Miscellaneous] 62 | # Set to true if you want to see which commands are sent to command line 63 | LogShellCommands = False 64 | 65 | # Ignore big (binary) files 66 | # Define a semicolon-separated list of extensions to be generally ignored 67 | # Example: 68 | # IgnoreFileExtensions = .zip; .jar; .exe; .dll 69 | IgnoreFileExtensions = 70 | 71 | # Optional: ignore directories 72 | # Define a semicolon-separated list of directories to be generally excluded 73 | # (we need the full path inside the resulting git repository) 74 | # Example: 75 | # IgnoreDirectories = projectX/WebContent/node_modules; projectY/distribution 76 | IgnoreDirectories = 77 | 78 | # Set to true if you want to include component root directories when loading the workspace 79 | # (this will add the -i / --include-root option to the (l)scm load command) 80 | IncludeComponentRoots = False 81 | -------------------------------------------------------------------------------- /configuration.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | import os 3 | import shlex 4 | import shutil 5 | 6 | import shell 7 | import shouter 8 | 9 | config = None 10 | configfile = None 11 | user = None 12 | password = None 13 | stored = None 14 | 15 | 16 | def read(configname=None): 17 | if not configname: 18 | global configfile 19 | configname = configfile 20 | parsedconfig = configparser.ConfigParser() 21 | if len(parsedconfig.read(configname)) < 1: 22 | raise IOError('unable to read %s' % configname) 23 | generalsection = parsedconfig['General'] 24 | migrationsectionname = 'Migration' 25 | migrationsection = parsedconfig[migrationsectionname] 26 | miscsectionname = 'Miscellaneous' 27 | global user 28 | if not user and not stored: 29 | user = generalsection['User'] 30 | global password 31 | if not password and not stored: 32 | password = generalsection['Password'] 33 | repositoryurl = generalsection['Repo'] 34 | scmcommand = generalsection.get('ScmCommand', "lscm") 35 | shell.logcommands = parsedconfig.get(miscsectionname, 'LogShellCommands', fallback="False") == "True" 36 | shell.setencoding(generalsection.get('encoding')) 37 | rtcversion = generalsection.get('RTCVersion', "5"); 38 | 39 | workspace = shlex.quote(generalsection['WorkspaceName']) 40 | gitreponame = generalsection['GIT-Reponame'] 41 | 42 | useexistingworkspace = generalsection.get('useExistingWorkspace', "False") 43 | useprovidedhistory = migrationsection.get('UseProvidedHistory', "False") 44 | useautomaticconflictresolution = migrationsection.get('UseAutomaticConflictResolution', "False") 45 | maxchangesetstoaccepttogether = migrationsection.get('MaxChangeSetsToAcceptTogether', "10") 46 | 47 | workdirectory = generalsection.get('Directory', os.getcwd()) 48 | streamname = shlex.quote(migrationsection['StreamToMigrate'].strip()) 49 | previousstreamname = migrationsection.get('PreviousStream', '').strip() 50 | baselines = getinitialcomponentbaselines(migrationsection.get('InitialBaseLines')) 51 | ignorefileextensionsproperty = parsedconfig.get(miscsectionname, 'IgnoreFileExtensions', fallback='') 52 | ignorefileextensions = parsesplittedproperty(ignorefileextensionsproperty) 53 | ignoredirectoriessproperty = parsedconfig.get(miscsectionname, 'IgnoreDirectories', fallback='') 54 | ignoredirectories = parsesplittedproperty(ignoredirectoriessproperty) 55 | includecomponentroots = parsedconfig.get(miscsectionname, 'IncludeComponentRoots', fallback="False") 56 | commitmessageprefix = migrationsection.get('CommitMessageWorkItemPrefix', "") 57 | gitattributesproperty = parsedconfig.get(migrationsectionname, 'Gitattributes', fallback='') 58 | gitattributes = parsesplittedproperty(gitattributesproperty) 59 | 60 | configbuilder = Builder().setuser(user).setpassword(password).setstored(stored).setrepourl(repositoryurl) 61 | configbuilder.setscmcommand(scmcommand).setrtcversion(rtcversion) 62 | configbuilder.setworkspace(workspace).setgitreponame(gitreponame).setrootfolder(os.getcwd()) 63 | configbuilder.setuseexistingworkspace(useexistingworkspace).setuseprovidedhistory(useprovidedhistory) 64 | configbuilder.setuseautomaticconflictresolution(useautomaticconflictresolution) 65 | configbuilder.setmaxchangesetstoaccepttogether(maxchangesetstoaccepttogether) 66 | configbuilder.setworkdirectory(workdirectory).setstreamname(streamname).setinitialcomponentbaselines(baselines) 67 | configbuilder.setpreviousstreamname(previousstreamname) 68 | configbuilder.setignorefileextensions(ignorefileextensions) 69 | configbuilder.setignoredirectories(ignoredirectories) 70 | configbuilder.setincludecomponentroots(includecomponentroots).setcommitmessageprefix(commitmessageprefix) 71 | configbuilder.setgitattributes(gitattributes) 72 | global config 73 | config = configbuilder.build() 74 | return config 75 | 76 | 77 | def get(): 78 | if not config: 79 | read() 80 | return config 81 | 82 | 83 | def setconfigfile(newconfigfile): 84 | global configfile 85 | configfile = newconfigfile 86 | 87 | 88 | def setUser(newuser): 89 | global user 90 | user = newuser 91 | 92 | 93 | def setPassword(newpassword): 94 | global password 95 | password = newpassword 96 | 97 | 98 | def setStored(newstored): 99 | global stored 100 | stored = newstored 101 | 102 | 103 | def getinitialcomponentbaselines(definedbaselines): 104 | initialcomponentbaselines = [] 105 | if definedbaselines: 106 | componentbaselines = definedbaselines.split(",") 107 | for entry in componentbaselines: 108 | componentbaseline = entry.split("=") 109 | component = componentbaseline[0].strip() 110 | baseline = componentbaseline[1].strip() 111 | initialcomponentbaselines.append(ComponentBaseLineEntry(component, baseline, component, baseline)) 112 | return initialcomponentbaselines 113 | 114 | 115 | def parsesplittedproperty(property, separator=';'): 116 | """ 117 | :param property 118 | :return: a list single properties, possibly empty 119 | """ 120 | properties = [] 121 | if property and len(property) > 0: 122 | for splittedproperty in property.split(separator): 123 | properties.append(splittedproperty.strip()) 124 | return properties 125 | 126 | 127 | class Builder: 128 | def __init__(self): 129 | self.user = "" 130 | self.password = "" 131 | self.stored = False 132 | self.repourl = "" 133 | self.scmcommand = "lscm" 134 | self.rtcversion = "" 135 | self.workspace = "" 136 | self.useexistingworkspace = "" 137 | self.useprovidedhistory = "" 138 | self.useautomaticconflictresolution = "" 139 | self.maxchangesetstoaccepttogether = "" 140 | self.workdirectory = os.path.dirname(os.path.realpath(__file__)) 141 | self.rootFolder = self.workdirectory 142 | self.logFolder = self.rootFolder + os.sep + "Logs" 143 | self.hasCreatedLogFolder = os.path.exists(self.logFolder) 144 | self.initialcomponentbaselines = "" 145 | self.streamname = "" 146 | self.gitreponame = "" 147 | self.clonedgitreponame = "" 148 | self.previousstreamname = "" 149 | self.ignorefileextensions = "" 150 | self.ignoredirectories = "" 151 | self.includecomponentroots = "" 152 | self.commitmessageprefix = "" 153 | self.gitattributes = "" 154 | 155 | def setuser(self, user): 156 | self.user = user 157 | return self 158 | 159 | def setpassword(self, password): 160 | self.password = password 161 | return self 162 | 163 | def setstored(self, stored): 164 | self.stored = stored 165 | return self 166 | 167 | def setrepourl(self, repourl): 168 | self.repourl = repourl 169 | return self 170 | 171 | def setscmcommand(self, scmcommand): 172 | self.scmcommand = scmcommand 173 | return self 174 | 175 | def setrtcversion(self, scmversion): 176 | self.rtcversion = int(scmversion) 177 | return self 178 | 179 | def setworkspace(self, workspace): 180 | self.workspace = workspace 181 | return self 182 | 183 | def setworkdirectory(self, workdirectory): 184 | self.workdirectory = workdirectory 185 | return self 186 | 187 | def setrootfolder(self, rootfolder): 188 | self.rootFolder = rootfolder 189 | return self 190 | 191 | def setlogfolder(self, logfolder): 192 | self.logFolder = logfolder 193 | return self 194 | 195 | def setinitialcomponentbaselines(self, initialcomponentbaselines): 196 | self.initialcomponentbaselines = initialcomponentbaselines 197 | return self 198 | 199 | def setstreamname(self, streamname): 200 | self.streamname = streamname 201 | return self 202 | 203 | def setgitreponame(self, reponame): 204 | self.gitreponame = reponame 205 | self.clonedgitreponame = reponame[:-4] # cut .git 206 | return self 207 | 208 | def setuseexistingworkspace(self, useexistingworkspace): 209 | self.useexistingworkspace = self.isenabled(useexistingworkspace) 210 | return self 211 | 212 | def setuseprovidedhistory(self, useprovidedhistory): 213 | self.useprovidedhistory = self.isenabled(useprovidedhistory) 214 | return self 215 | 216 | def setuseautomaticconflictresolution(self, useautomaticconflictresolution): 217 | self.useautomaticconflictresolution = self.isenabled(useautomaticconflictresolution) 218 | return self 219 | 220 | def setmaxchangesetstoaccepttogether(self, maxchangesetstoaccepttogether): 221 | self.maxchangesetstoaccepttogether = int(maxchangesetstoaccepttogether) 222 | return self 223 | 224 | def setpreviousstreamname(self, previousstreamname): 225 | self.previousstreamname = previousstreamname 226 | return self 227 | 228 | def setignorefileextensions(self, ignorefileextensions): 229 | self.ignorefileextensions = ignorefileextensions 230 | return self 231 | 232 | def setignoredirectories(self, ignoreirectories): 233 | self.ignoredirectories = ignoreirectories 234 | return self 235 | 236 | def setincludecomponentroots(self, includecomponentroots): 237 | self.includecomponentroots = self.isenabled(includecomponentroots) 238 | return self 239 | 240 | def setcommitmessageprefix(self, commitmessageprefix): 241 | self.commitmessageprefix = commitmessageprefix 242 | return self 243 | 244 | def setgitattributes(self, gitattributes): 245 | self.gitattributes = gitattributes 246 | return self 247 | 248 | @staticmethod 249 | def isenabled(stringwithbooleanexpression): 250 | return stringwithbooleanexpression == "True" 251 | 252 | def build(self): 253 | return ConfigObject(self.user, self.password, self.stored, self.repourl, self.scmcommand, self.rtcversion, 254 | self.workspace, 255 | self.useexistingworkspace, self.workdirectory, self.initialcomponentbaselines, 256 | self.streamname, self.gitreponame, self.useprovidedhistory, 257 | self.useautomaticconflictresolution, self.maxchangesetstoaccepttogether, self.clonedgitreponame, self.rootFolder, 258 | self.previousstreamname, self.ignorefileextensions, self.ignoredirectories, self.includecomponentroots, 259 | self.commitmessageprefix, self.gitattributes) 260 | 261 | 262 | class ConfigObject: 263 | 264 | def __init__(self, user, password, stored, repourl, scmcommand, rtcversion, workspace, useexistingworkspace, 265 | workdirectory, 266 | initialcomponentbaselines, streamname, gitreponame, useprovidedhistory, 267 | useautomaticconflictresolution, maxchangesetstoaccepttogether, clonedgitreponame, rootfolder, previousstreamname, 268 | ignorefileextensions, ignoredirectories, includecomponentroots, commitmessageprefix, gitattributes): 269 | self.user = user 270 | self.password = password 271 | self.stored = stored 272 | self.repo = repourl 273 | self.scmcommand = scmcommand 274 | self.rtcversion = rtcversion 275 | self.workspace = workspace 276 | self.useexistingworkspace = useexistingworkspace 277 | self.useprovidedhistory = useprovidedhistory 278 | self.useautomaticconflictresolution = useautomaticconflictresolution 279 | self.maxchangesetstoaccepttogether = maxchangesetstoaccepttogether 280 | self.workDirectory = workdirectory 281 | self.initialcomponentbaselines = initialcomponentbaselines 282 | self.streamname = streamname 283 | self.gitRepoName = gitreponame 284 | self.clonedGitRepoName = clonedgitreponame 285 | self.rootFolder = rootfolder 286 | self.logFolder = rootfolder + os.sep + "Logs" 287 | self.hasCreatedLogFolder = os.path.exists(self.logFolder) 288 | self.streamuuid = "" 289 | self.previousstreamname = previousstreamname 290 | self.previousstreamuuid = "" 291 | self.ignorefileextensions = ignorefileextensions 292 | self.ignoredirectories = ignoredirectories 293 | self.includecomponentroots = includecomponentroots 294 | self.commitmessageprefix = commitmessageprefix 295 | self.gitattributes = gitattributes 296 | 297 | def getlogpath(self, filename): 298 | if not self.hasCreatedLogFolder: 299 | os.makedirs(self.logFolder) 300 | self.hasCreatedLogFolder = True 301 | return self.logFolder + os.sep + filename 302 | 303 | def deletelogfolder(self): 304 | if self.hasCreatedLogFolder: 305 | shutil.rmtree(self.logFolder) 306 | self.hasCreatedLogFolder = False 307 | 308 | def gethistorypath(self, filename): 309 | historypath = self.rootFolder + os.sep + "History" 310 | return historypath + os.sep + filename 311 | 312 | def collectstreamuuid(self, streamname): 313 | if not streamname: 314 | return 315 | shouter.shout("Get UUID of configured stream " + streamname) 316 | showuuidcommand = "%s --show-alias n --show-uuid y show attributes -r %s -w %s" % ( 317 | self.scmcommand, self.repo, streamname) 318 | output = shell.getoutput(showuuidcommand) 319 | splittedfirstline = output[0].split(" ") 320 | streamuuid = splittedfirstline[0].strip()[1:-1] 321 | return streamuuid 322 | 323 | def collectstreamuuids(self): 324 | self.streamuuid = self.collectstreamuuid(self.streamname) 325 | self.previousstreamuuid = self.collectstreamuuid(self.previousstreamname) 326 | 327 | 328 | class ComponentBaseLineEntry: 329 | def __init__(self, component, baseline, componentname, baselinename): 330 | self.component = component 331 | self.baseline = baseline 332 | self.componentname = componentname 333 | self.baselinename = baselinename 334 | -------------------------------------------------------------------------------- /gitFunctions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | from datetime import datetime 4 | 5 | import configuration 6 | import shell 7 | import shouter 8 | 9 | 10 | class Initializer: 11 | def __init__(self): 12 | config = configuration.get() 13 | self.repoName = config.gitRepoName 14 | self.clonedRepoName = config.clonedGitRepoName 15 | self.author = config.user 16 | 17 | @staticmethod 18 | def createignore(): 19 | git_ignore = ".gitignore" 20 | 21 | if not os.path.exists(git_ignore): 22 | with open(git_ignore, "w") as ignore: 23 | ignore.write(".jazz5" + '\n') 24 | ignore.write(".metadata" + '\n') 25 | ignore.write(".jazzShed" + '\n') 26 | config = configuration.get() 27 | if len(config.ignoredirectories) > 0: 28 | ignore.write('\n' + "# directories" + '\n') 29 | for directory in config.ignoredirectories: 30 | ignore.write('/' + directory + '\n') 31 | ignore.write('\n') 32 | shell.execute("git add " + git_ignore) 33 | shell.execute("git commit -m %s -q" % shell.quote("Add .gitignore")) 34 | 35 | @staticmethod 36 | def createattributes(): 37 | """ 38 | create a .gitattributes file (if so specified and not yet present) 39 | """ 40 | config = configuration.get() 41 | if len(config.gitattributes) > 0: 42 | gitattribues = ".gitattributes" 43 | if not os.path.exists(gitattribues): 44 | with open(gitattribues, "w") as attributes: 45 | for line in config.gitattributes: 46 | attributes.write(line + '\n') 47 | shell.execute("git add " + gitattribues) 48 | shell.execute("git commit -m %s -q" % shell.quote("Add .gitattributes")) 49 | 50 | def initalize(self): 51 | self.createrepo() 52 | self.preparerepo() 53 | 54 | @staticmethod 55 | def preparerepo(): 56 | Initializer.setgitconfigs() 57 | Initializer.createignore() 58 | Initializer.createattributes() 59 | 60 | def createrepo(self): 61 | shell.execute("git init --bare " + self.repoName) 62 | shouter.shout("Repository was created in " + os.getcwd()) 63 | shell.execute("git clone " + self.repoName) 64 | os.chdir(self.clonedRepoName) 65 | 66 | @staticmethod 67 | def setgitconfigs(): 68 | shell.execute("git config push.default current") 69 | shell.execute("git config core.ignorecase false") # should be the default anyway 70 | shouter.shout("Set core.ignorecase to false") 71 | 72 | @staticmethod 73 | def initialcommit(): 74 | shouter.shout("Initial git add") 75 | shell.execute("git add -A", os.devnull) 76 | shouter.shout("Finished initial git add, starting commit") 77 | shell.execute("git commit -m %s -q" % shell.quote("Initial Commit")) 78 | shouter.shout("Finished initial commit") 79 | 80 | 81 | class Commiter: 82 | commitcounter = 0 83 | isattachedtoaworkitemregex = re.compile("^\d*:.*-") 84 | findignorepatternregex = re.compile("\{([^\{\}]*)\}") 85 | 86 | @staticmethod 87 | def addandcommit(changeentry): 88 | Commiter.handleignore() 89 | Commiter.replaceauthor(changeentry.author, changeentry.email) 90 | shell.execute("git add -A") 91 | 92 | Commiter.handle_captitalization_filename_changes() 93 | 94 | shell.execute(Commiter.getcommitcommand(changeentry)) 95 | Commiter.commitcounter += 1 96 | if Commiter.commitcounter is 30: 97 | shouter.shout("30 Commits happend, push current branch to avoid out of memory") 98 | Commiter.pushbranch("") 99 | Commiter.commitcounter = 0 100 | shouter.shout("Commited change in local git repository") 101 | 102 | @staticmethod 103 | def handle_captitalization_filename_changes(): 104 | sandbox = os.path.join(configuration.get().workDirectory, configuration.get().clonedGitRepoName) 105 | lines = shell.getoutput("git status -z", stripped=False) 106 | for newfilerelativepath in Commiter.splitoutputofgitstatusz(lines, "A "): 107 | directoryofnewfile = os.path.dirname(os.path.join(sandbox, newfilerelativepath)) 108 | newfilename = os.path.basename(newfilerelativepath) 109 | cwd = os.getcwd() 110 | os.chdir(directoryofnewfile) 111 | files = shell.getoutput("git ls-files") 112 | for previousFileName in files: 113 | was_same_file_name = newfilename.lower() == previousFileName.lower() 114 | file_was_renamed = newfilename != previousFileName 115 | 116 | if was_same_file_name and file_was_renamed: 117 | shell.execute("git rm --cached %s" % previousFileName) 118 | os.chdir(cwd) 119 | 120 | @staticmethod 121 | def getcommitcommand(changeentry): 122 | comment = Commiter.getcommentwithprefix(changeentry.comment) 123 | return "git commit -m %s --date %s --author=%s" \ 124 | % (shell.quote(comment), shell.quote(changeentry.date), changeentry.getgitauthor()) 125 | 126 | @staticmethod 127 | def getcommentwithprefix(comment): 128 | prefix = configuration.get().commitmessageprefix 129 | 130 | if prefix and Commiter.isattachedtoaworkitemregex.match(comment): 131 | return prefix + comment 132 | return comment 133 | 134 | @staticmethod 135 | def replaceauthor(author, email): 136 | shell.execute("git config --replace-all user.name " + shell.quote(author)) 137 | if not email: 138 | email = Commiter.defaultemail(author) 139 | shell.execute("git config --replace-all user.email " + email) 140 | 141 | @staticmethod 142 | def defaultemail(author): 143 | if not author: 144 | name = "default" 145 | else: 146 | haspoint = False 147 | index = 0 148 | name = "" 149 | for c in author: 150 | if c.isalnum() or c == "_": 151 | name += c 152 | else: 153 | if index > 0 and not haspoint: 154 | name += "." 155 | haspoint = True 156 | else: 157 | name += "_" 158 | index += 1 159 | return name.lower() + "@rtc.to" 160 | 161 | @staticmethod 162 | def checkbranchname(branchname): 163 | exitcode = shell.execute("git check-ref-format --normalize refs/heads/" + branchname) 164 | if exitcode is 0: 165 | return True 166 | else: 167 | return False 168 | 169 | @staticmethod 170 | def branch(branchname): 171 | branchexist = shell.execute("git show-ref --verify --quiet refs/heads/" + branchname) 172 | if branchexist is 0: 173 | Commiter.checkout(branchname) 174 | else: 175 | shell.execute("git checkout -b " + branchname) 176 | 177 | @staticmethod 178 | def pushbranch(branchname, force=False): 179 | if branchname: 180 | shouter.shout("Push of branch " + branchname) 181 | if force: 182 | return shell.execute("git push -f origin " + branchname) 183 | else: 184 | return shell.execute("git push origin " + branchname) 185 | 186 | @staticmethod 187 | def pushmaster(): 188 | Commiter.pushbranch("master") 189 | 190 | @staticmethod 191 | def checkout(branchname): 192 | shell.execute("git checkout " + branchname) 193 | 194 | @staticmethod 195 | def renamebranch(oldname, newname): 196 | return shell.execute("git branch -m %s %s" % (oldname, newname)) 197 | 198 | @staticmethod 199 | def copybranch(existingbranchname, newbranchname): 200 | return shell.execute("git branch %s %s" % (newbranchname, existingbranchname)) 201 | 202 | @staticmethod 203 | def promotebranchtomaster(branchname): 204 | master = "master" 205 | masterrename = Commiter.renamebranch(master, "masterRenamedAt_" + datetime.now().strftime('%Y%m%d_%H%M%S')) 206 | copybranch = Commiter.copybranch(branchname, master) 207 | 208 | if masterrename is 0 and copybranch is 0: 209 | return Commiter.pushbranch(master, True) 210 | else: 211 | shouter.shout("Branch %s couldnt get renamed to master, please do that on your own" % branchname) 212 | return 1 # branch couldnt get renamed 213 | 214 | @staticmethod 215 | def get_untracked_statuszlines(): 216 | return shell.getoutput("git status --untracked-files=all -z", stripped=False) 217 | 218 | 219 | @staticmethod 220 | def handleignore(): 221 | """ 222 | check untracked files and handle both global and local ignores 223 | """ 224 | repositoryfiles = Commiter.splitoutputofgitstatusz(Commiter.get_untracked_statuszlines()) 225 | Commiter.ignoreextensions(repositoryfiles) 226 | Commiter.ignorejazzignore(repositoryfiles) 227 | 228 | @staticmethod 229 | def ignoreextensions(repositoryfiles): 230 | """ 231 | add files with extensions to be ignored to the global .gitignore 232 | """ 233 | ignorefileextensions = configuration.get().ignorefileextensions 234 | if len(ignorefileextensions) > 0: 235 | Commiter.ignore(ExtensionFilter.match(repositoryfiles, ignorefileextensions)) 236 | 237 | @staticmethod 238 | def ignore(filelines): 239 | """ 240 | append the file lines to the toplevel .gitignore 241 | :param filelines: a list of newline terminated file names to be ignored 242 | """ 243 | if len(filelines) > 0: 244 | with open(".gitignore", "a") as ignore: 245 | ignore.writelines(filelines) 246 | 247 | @staticmethod 248 | def splitoutputofgitstatusz(lines, filterprefix=None): 249 | """ 250 | Split the output of 'git status -z' into single files 251 | 252 | :param lines: the unstripped output line(s) from the command 253 | :param filterprefix: if given, only the files of those entries matching the prefix will be returned 254 | :return: a list of repository files with status changes 255 | """ 256 | repositoryfiles = [] 257 | for line in lines: # expect exactly one line 258 | entries = line.split(sep='\x00') # ascii 0 is the delimiter 259 | for entry in entries: 260 | if len(entry) > 0: 261 | if not filterprefix or entry.startswith(filterprefix): 262 | start = entry.find(' ') 263 | if 0 <= start <= 2: 264 | repositoryfile = entry[3:] # output is formatted 265 | else: 266 | repositoryfile = entry # file on a single line (e.g. rename continuation) 267 | repositoryfiles.append(repositoryfile) 268 | return repositoryfiles 269 | 270 | @staticmethod 271 | def translatejazzignore(jazzignorelines): 272 | """ 273 | translate the lines of a local .jazzignore file into the lines of a local .gitignore file 274 | 275 | :param jazzignorelines: the input lines 276 | :return: the .gitignore lines 277 | """ 278 | recursive = False 279 | gitignorelines = [] 280 | for line in jazzignorelines: 281 | if not line.startswith("#"): 282 | line = line.strip() 283 | if line.startswith("core.ignore"): 284 | gitignorelines.append('\n') 285 | recursive = line.startswith("core.ignore.recursive") 286 | for foundpattern in Commiter.findignorepatternregex.findall(line): 287 | gitignoreline = foundpattern + '\n' 288 | if not recursive: 289 | gitignoreline = '/' + gitignoreline # forward, not os.sep 290 | gitignorelines.append(gitignoreline) 291 | return gitignorelines 292 | 293 | @staticmethod 294 | def restore_shed_gitignore(statuszlines): 295 | """ 296 | If a force reload of the RTC workspace sheds .gitignore files away, we need to restore them. 297 | In this case they are marked as deletions from git. 298 | 299 | :param statuszlines: the git status z output lines 300 | """ 301 | gitignore = ".gitignore" 302 | gitignorelen = len(gitignore) 303 | deletedfiles = Commiter.splitoutputofgitstatusz(statuszlines, " D ") 304 | for deletedfile in deletedfiles: 305 | if deletedfile[-gitignorelen:] == gitignore: 306 | # only restore .gitignore if sibling .jazzignore still exists 307 | jazzignorefile = deletedfile[:-gitignorelen] + ".jazzignore" 308 | if os.path.exists(jazzignorefile): 309 | shell.execute("git checkout -- %s" % deletedfile) 310 | 311 | @staticmethod 312 | def ignorejazzignore(repositoryfiles): 313 | """ 314 | If a .jazzignore file is modified or added, translate it to .gitignore, 315 | if a .jazzignore file is deleted, delete the corresponding .gitignore file as well. 316 | 317 | :param repositoryfiles: the modified files 318 | """ 319 | jazzignore = ".jazzignore" 320 | jazzignorelen = len(jazzignore) 321 | for repositoryfile in repositoryfiles: 322 | if repositoryfile[-jazzignorelen:] == jazzignore: 323 | path = repositoryfile[0:len(repositoryfile)-jazzignorelen] 324 | gitignore = path + ".gitignore" 325 | if os.path.exists(repositoryfile): 326 | # update (or create) .gitignore 327 | jazzignorelines = [] 328 | with open(repositoryfile, 'r') as jazzignorefile: 329 | jazzignorelines = jazzignorefile.readlines() 330 | if len(jazzignorelines) > 0: 331 | # overwrite in any case 332 | with open(gitignore, 'w') as gitignorefile: 333 | gitignorefile.writelines(Commiter.translatejazzignore(jazzignorelines)) 334 | else: 335 | # delete .gitignore 336 | if os.path.exists(gitignore): 337 | os.remove(gitignore) 338 | 339 | 340 | class Differ: 341 | @staticmethod 342 | def has_diff(): 343 | return shell.execute("git diff --quiet") is 1 344 | 345 | 346 | class ExtensionFilter: 347 | 348 | @staticmethod 349 | def match(repositoryfiles, extensions): 350 | """ 351 | Determine the repository files to ignore. 352 | These filenames are returned as a list of newline terminated lines, 353 | ready to be added to .gitignore with writelines() 354 | 355 | :param repositoryfiles: a list of (changed) files 356 | :param extensions the extensions to be ignored 357 | :return: a list of newline terminated file names, possibly empty 358 | """ 359 | repositoryfilestoignore = [] 360 | for extension in extensions: 361 | for repositoryfile in repositoryfiles: 362 | extlen = len(extension) 363 | if len(repositoryfile) >= extlen: 364 | if repositoryfile[-extlen:] == extension: 365 | # prepend a forward slash (for non recursive,) 366 | # escape a backslash with a backslash 367 | # append a newline 368 | repositoryfilestoignore.append('/' + repositoryfile.replace('\\', '\\\\') + '\n') 369 | return repositoryfilestoignore 370 | -------------------------------------------------------------------------------- /migration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import argparse 4 | import os 5 | import sys 6 | 7 | import configuration 8 | import shouter 9 | from gitFunctions import Commiter 10 | from gitFunctions import Initializer, Differ 11 | from rtcFunctions import ImportHandler 12 | from rtcFunctions import RTCInitializer 13 | from rtcFunctions import RTCLogin 14 | from rtcFunctions import WorkspaceHandler 15 | 16 | 17 | def initialize(): 18 | config = configuration.get() 19 | directory = config.workDirectory 20 | if os.path.exists(directory): 21 | sys.exit("Configured directory '" + directory + "' already exists, please make sure to use a " 22 | + "non-existing directory") 23 | shouter.shout("Migration will take place in " + directory) 24 | os.makedirs(directory) 25 | os.chdir(directory) 26 | config.deletelogfolder() 27 | git = Initializer() 28 | git.initalize() 29 | RTCInitializer.initialize() 30 | if Differ.has_diff(): 31 | git.initialcommit() 32 | Commiter.pushmaster() 33 | 34 | 35 | def resume(): 36 | shouter.shout("Found existing git repo in work directory, resuming migration...") 37 | config = configuration.get() 38 | os.chdir(config.workDirectory) 39 | os.chdir(config.clonedGitRepoName) 40 | if Differ.has_diff(): 41 | sys.exit("Your git repo has some uncommited changes, please add/remove them manually") 42 | RTCLogin.loginandcollectstreamuuid() 43 | Initializer.preparerepo() 44 | if config.previousstreamname: 45 | prepare() 46 | else: 47 | Commiter.branch(config.streamname) 48 | WorkspaceHandler().load() 49 | 50 | 51 | def existsrepo(): 52 | config = configuration.get() 53 | repodirectory = os.path.join(config.workDirectory, config.gitRepoName) 54 | return os.path.exists(repodirectory) 55 | 56 | 57 | def migrate(): 58 | rtc = ImportHandler() 59 | rtcworkspace = WorkspaceHandler() 60 | git = Commiter 61 | 62 | if existsrepo(): 63 | resume() 64 | else: 65 | initialize() 66 | 67 | config = configuration.get() 68 | streamuuid = config.streamuuid 69 | streamname = config.streamname 70 | branchname = streamname + "_branchpoint" 71 | 72 | componentbaselineentries = rtc.getcomponentbaselineentriesfromstream(streamuuid) 73 | rtcworkspace.setnewflowtargets(streamuuid) 74 | 75 | history = rtc.readhistory(componentbaselineentries, streamname) 76 | changeentries = rtc.getchangeentriesofstreamcomponents(componentbaselineentries) 77 | 78 | if len(changeentries) > 0: 79 | git.branch(branchname) 80 | rtc.acceptchangesintoworkspace(rtc.getchangeentriestoaccept(changeentries, history)) 81 | shouter.shout("All changes until creation of stream '%s' accepted" % streamname) 82 | git.pushbranch(branchname) 83 | 84 | rtcworkspace.setcomponentstobaseline(componentbaselineentries, streamuuid) 85 | rtcworkspace.load() 86 | 87 | git.branch(streamname) 88 | changeentries = rtc.getchangeentriesofstream(streamuuid) 89 | amountofacceptedchanges = rtc.acceptchangesintoworkspace(rtc.getchangeentriestoaccept(changeentries, history)) 90 | if amountofacceptedchanges > 0: 91 | git.pushbranch(streamname) 92 | git.promotebranchtomaster(streamname) 93 | 94 | RTCLogin.logout() 95 | summary(streamname) 96 | 97 | 98 | def prepare(): 99 | config = configuration.get() 100 | rtc = ImportHandler() 101 | rtcworkspace = WorkspaceHandler() 102 | # git checkout branchpoint 103 | Commiter.checkout(config.previousstreamname + "_branchpoint") 104 | # list baselines of current workspace 105 | componentbaselineentries = rtc.getcomponentbaselineentriesfromstream(config.previousstreamuuid) 106 | # set components to that baselines 107 | rtcworkspace.setcomponentstobaseline(componentbaselineentries, config.previousstreamuuid) 108 | rtcworkspace.load() 109 | 110 | 111 | def summary(streamname): 112 | config = configuration.get() 113 | shouter.shout("\nAll changes accepted - Migration of stream '%s' is completed." 114 | "\nYou can distribute the git-repo '%s'." % (streamname, config.gitRepoName)) 115 | if len(config.ignorefileextensions) > 0: 116 | # determine and log the ignored but still present files 117 | os.chdir(config.workDirectory) 118 | os.chdir(config.clonedGitRepoName) 119 | pathtoclonedgitrepo = config.workDirectory + os.sep + config.clonedGitRepoName 120 | if pathtoclonedgitrepo[-1:] != os.sep: 121 | pathtoclonedgitrepo += os.sep 122 | ignoredbutexist = [] 123 | with open('.gitignore', 'r') as gitignore: 124 | for line in gitignore.readlines(): 125 | line = line.strip() 126 | if line != ".jazz5" and line != ".metadata" and line != ".jazzShed": 127 | pathtoignored = pathtoclonedgitrepo + line 128 | if os.path.exists(pathtoignored): 129 | ignoredbutexist.append(line) 130 | if len(ignoredbutexist) > 0: 131 | shouter.shout("\nThe following files have been ignored in the new git repository, " + 132 | "but still exist in the actual RTC workspace:") 133 | ignoredbutexist.sort() 134 | for ignored in ignoredbutexist: 135 | shouter.shout("\t" + ignored) 136 | 137 | 138 | def parsecommandline(): 139 | parser = argparse.ArgumentParser() 140 | configfiledefault = 'config.ini' 141 | configfilehelp = 'name of the config file, or full path to the config file; defaults to ' + configfiledefault 142 | parser.add_argument('-c', '--configfile', metavar='file', dest='configfile', help=configfilehelp, 143 | default=configfiledefault) 144 | parser.add_argument('-u', '--user', metavar='user', dest='user', help='RTC user', default=None) 145 | parser.add_argument('-p', '--password', metavar='password', dest='password', help='RTC password', default=None) 146 | parser.add_argument('-s', '--stored', help='Use stored password for the repository connection', action='store_true') 147 | arguments = parser.parse_args() 148 | configuration.setconfigfile(arguments.configfile) 149 | configuration.setUser(arguments.user) 150 | configuration.setPassword(arguments.password) 151 | configuration.setStored(arguments.stored) 152 | 153 | 154 | def validate(): 155 | config = configuration.get() 156 | streamname = config.streamname 157 | branchname = streamname + "_branchpoint" 158 | previousstreamname = config.previousstreamname 159 | offendingbranchname = None 160 | if not Commiter.checkbranchname(streamname): 161 | offendingbranchname = streamname 162 | elif not Commiter.checkbranchname(branchname): 163 | offendingbranchname = branchname 164 | elif not Commiter.checkbranchname(previousstreamname): 165 | offendingbranchname = previousstreamname 166 | if offendingbranchname: 167 | sys.exit(offendingbranchname + " is not a valid git branch name - consider renaming the stream") 168 | 169 | 170 | if __name__ == "__main__": 171 | parsecommandline() 172 | validate() 173 | migrate() 174 | -------------------------------------------------------------------------------- /rtcFunctions.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | from enum import Enum, unique 5 | 6 | import configuration 7 | import shell 8 | import shouter 9 | import sorter 10 | from configuration import ComponentBaseLineEntry 11 | from gitFunctions import Commiter, Differ 12 | 13 | loginCredentialsCommand = "-u '%s' -P '%s'" 14 | 15 | class RTCInitializer: 16 | @staticmethod 17 | def initialize(): 18 | RTCLogin.loginandcollectstreamuuid() 19 | workspace = WorkspaceHandler() 20 | config = configuration.get() 21 | if config.useexistingworkspace: 22 | shouter.shout("Use existing workspace to start migration") 23 | workspace.load() 24 | else: 25 | workspace.createandload(config.streamuuid, config.initialcomponentbaselines) 26 | 27 | 28 | class RTCLogin: 29 | @staticmethod 30 | def loginandcollectstreamuuid(): 31 | global loginCredentialsCommand 32 | config = configuration.get() 33 | if not config.stored: 34 | loginHeaderCommand = "%s login -r %s " 35 | exitcode = shell.execute((loginHeaderCommand + loginCredentialsCommand) % (config.scmcommand, config.repo, config.user, config.password)) 36 | if exitcode is not 0: 37 | shouter.shout("Login failed. Trying again without quotes.") 38 | loginCredentialsCommand = "-u %s -P %s" 39 | exitcode = shell.execute((loginHeaderCommand + loginCredentialsCommand) % (config.scmcommand, config.repo, config.user, config.password)) 40 | if exitcode is not 0: 41 | sys.exit("Login failed. Please check your connection and credentials.") 42 | config.collectstreamuuids() 43 | 44 | @staticmethod 45 | def logout(): 46 | config = configuration.get() 47 | if not config.stored: 48 | shell.execute("%s logout -r %s" % (config.scmcommand, config.repo)) 49 | 50 | 51 | class WorkspaceHandler: 52 | def __init__(self): 53 | self.config = configuration.get() 54 | self.workspace = self.config.workspace 55 | self.repo = self.config.repo 56 | self.scmcommand = self.config.scmcommand 57 | self.rtcversion = self.config.rtcversion 58 | 59 | def createandload(self, stream, componentbaselineentries=[]): 60 | shell.execute("%s create workspace -r %s -s %s %s" % (self.scmcommand, self.repo, stream, self.workspace)) 61 | if componentbaselineentries: 62 | self.setcomponentstobaseline(componentbaselineentries, stream) 63 | else: 64 | self.setcomponentstobaseline(ImportHandler().determineinitialbaseline(stream), 65 | stream) 66 | self.load() 67 | 68 | def load(self): 69 | command = "%s load -r %s %s --force" % (self.scmcommand, self.repo, self.workspace) 70 | if self.config.includecomponentroots: 71 | command += " --include-root" 72 | shouter.shout("Start (re)loading current workspace: " + command) 73 | shell.execute(command) 74 | shouter.shout("Load of workspace finished") 75 | Commiter.restore_shed_gitignore(Commiter.get_untracked_statuszlines()) 76 | 77 | 78 | def setcomponentstobaseline(self, componentbaselineentries, streamuuid): 79 | for entry in componentbaselineentries: 80 | shouter.shout("Set component '%s'(%s) to baseline '%s' (%s)" % (entry.componentname, entry.component, 81 | entry.baselinename, entry.baseline)) 82 | 83 | replacecommand = "%s set component -r %s -b %s %s stream %s %s --overwrite-uncommitted" % \ 84 | (self.scmcommand, self.repo, entry.baseline, self.workspace, streamuuid, entry.component) 85 | shell.execute(replacecommand) 86 | 87 | def setnewflowtargets(self, streamuuid): 88 | shouter.shout("Set new Flowtargets") 89 | if not self.hasflowtarget(streamuuid): 90 | shell.execute("%s add flowtarget -r %s %s %s" % (self.scmcommand, self.repo, self.workspace, streamuuid)) 91 | 92 | flowarg = "" 93 | if self.rtcversion >= 6: 94 | # Need to specify an arg to default and current option or 95 | # set flowtarget command will fail. 96 | # Assume that this is mandatory for RTC version >= 6.0.0 97 | flowarg = "b" 98 | command = "%s set flowtarget -r %s %s --default %s --current %s %s" % (self.scmcommand, self.repo, self.workspace, 99 | flowarg, flowarg, streamuuid) 100 | shell.execute(command) 101 | 102 | def hasflowtarget(self, streamuuid): 103 | command = "%s --show-uuid y --show-alias n list flowtargets -r %s %s" % (self.scmcommand, self.repo, self.workspace) 104 | flowtargetlines = shell.getoutput(command) 105 | for flowtargetline in flowtargetlines: 106 | splittedinformationline = flowtargetline.split("\"") 107 | uuidpart = splittedinformationline[0].split(" ") 108 | flowtargetuuid = uuidpart[0].strip()[1:-1] 109 | if streamuuid in flowtargetuuid: 110 | return True 111 | return False 112 | 113 | 114 | class Changes: 115 | 116 | latest_accept_command = "" 117 | 118 | @staticmethod 119 | def discard(*changeentries): 120 | config = configuration.get() 121 | idstodiscard = Changes._collectids(changeentries) 122 | exitcode = shell.execute(config.scmcommand + " discard -w " + config.workspace + " -r " + config.repo + " -o" + idstodiscard) 123 | if exitcode is 0: 124 | for changeEntry in changeentries: 125 | changeEntry.setUnaccepted() 126 | 127 | @staticmethod 128 | def accept(logpath, *changeentries): 129 | for changeEntry in changeentries: 130 | shouter.shout("Accepting: " + changeEntry.tostring()) 131 | revisions = Changes._collectids(changeentries) 132 | config = configuration.get() 133 | Changes.latest_accept_command = config.scmcommand + " accept --verbose --overwrite-uncommitted --accept-missing-changesets --no-merge --repository-uri " + config.repo + " --target " + \ 134 | config.workspace + " --changes" + revisions 135 | exitcode = shell.execute(Changes.latest_accept_command, logpath, "a") 136 | if exitcode is 0: 137 | for changeEntry in changeentries: 138 | changeEntry.setAccepted() 139 | return True 140 | else: 141 | return False 142 | 143 | @staticmethod 144 | def _collectids(changeentries): 145 | ids = "" 146 | for changeentry in changeentries: 147 | ids += " " + changeentry.revision 148 | return ids 149 | 150 | @staticmethod 151 | def tostring(*changes): 152 | logmessage = "Changes: \n" 153 | for change in changes: 154 | logmessage += change.tostring() + "\n" 155 | shouter.shout(logmessage) 156 | 157 | 158 | class ImportHandler: 159 | def __init__(self): 160 | self.config = configuration.get() 161 | self.acceptlogpath = self.config.getlogpath("accept.txt") 162 | 163 | def getcomponentbaselineentriesfromstream(self, stream): 164 | filename = self.config.getlogpath("StreamComponents_" + stream + ".txt") 165 | command = "%s --show-alias n --show-uuid y list components -v -m 30 -r %s %s" % (self.config.scmcommand, 166 | self.config.repo, stream) 167 | shell.execute(command, filename) 168 | componentbaselinesentries = [] 169 | skippedfirstrow = False 170 | islinewithcomponent = 2 171 | component = "" 172 | baseline = "" 173 | componentname = "" 174 | baselinename = "" 175 | 176 | with open(filename, 'r', encoding=shell.encoding) as file: 177 | for line in file: 178 | cleanedline = line.strip() 179 | if cleanedline: 180 | if not skippedfirstrow: 181 | skippedfirstrow = True 182 | continue 183 | splittedinformationline = line.split("\"") 184 | uuidpart = splittedinformationline[0].split(" ") 185 | if islinewithcomponent % 2 is 0: 186 | component = uuidpart[3].strip()[1:-1] 187 | componentname = splittedinformationline[1] 188 | else: 189 | if self.config.rtcversion >= 6: 190 | # fix trim brackets for vers. 6.x.x 191 | baseline = uuidpart[7].strip()[1:-1] 192 | else: 193 | baseline = uuidpart[5].strip()[1:-1] 194 | baselinename = splittedinformationline[1] 195 | 196 | if baseline and component: 197 | componentbaselinesentries.append( 198 | ComponentBaseLineEntry(component, baseline, componentname, baselinename)) 199 | baseline = "" 200 | component = "" 201 | componentname = "" 202 | baselinename = "" 203 | islinewithcomponent += 1 204 | return componentbaselinesentries 205 | 206 | def determineinitialbaseline(self, stream): 207 | regex = "\(_[\w-]+\)" 208 | pattern = re.compile(regex) 209 | config = self.config 210 | componentbaselinesentries = self.getcomponentbaselineentriesfromstream(stream) 211 | logincredentials = "" 212 | if not config.stored: 213 | logincredentials = loginCredentialsCommand % (config.user, config.password) 214 | for entry in componentbaselinesentries: 215 | shouter.shout("Determine initial baseline of " + entry.componentname) 216 | # use always scm, lscm fails when specifying maximum over 10k 217 | command = "scm --show-alias n --show-uuid y list baselines --components %s -r %s %s -m 20000" % \ 218 | (entry.component, config.repo, logincredentials) 219 | baselineslines = shell.getoutput(command) 220 | baselineslines.reverse() # reverse to have earliest baseline on top 221 | 222 | for baselineline in baselineslines: 223 | matcher = pattern.search(baselineline) 224 | if matcher: 225 | matchedstring = matcher.group() 226 | uuid = matchedstring[1:-1] 227 | entry.baseline = uuid 228 | entry.baselinename = "Automatically detected initial baseline" 229 | shouter.shout("Initial baseline is: %s" % baselineline) 230 | break 231 | return componentbaselinesentries 232 | 233 | def acceptchangesintoworkspace(self, changeentries): 234 | amountofchanges = len(changeentries) 235 | if amountofchanges == 0: 236 | shouter.shout("Found no changes to accept") 237 | else: 238 | shouter.shoutwithdate("Start accepting %s changesets" % amountofchanges) 239 | amountofacceptedchanges = 0 240 | 241 | for changeEntry in changeentries: 242 | amountofacceptedchanges += 1 243 | if not changeEntry.isAccepted(): # change could already be accepted from a retry 244 | if not Changes.accept(self.acceptlogpath, changeEntry): 245 | shouter.shout( 246 | "Change wasnt succesfully accepted into workspace, please load your workspace in eclipse and check whats wrong") 247 | self.is_user_aborting(changeEntry) 248 | # self.retryacceptincludingnextchangesets(changeEntry, changeentries) 249 | if not Differ.has_diff(): 250 | # no differences found - force reload of the workspace 251 | shouter.shout("No changes for commiting in git detected, going to reload the workspace") 252 | WorkspaceHandler().load() 253 | if not Differ.has_diff(): 254 | shouter.shout("Still no changes... Please load your workspace in eclipse and check whats wrong") 255 | # still no differences, something wrong 256 | self.is_user_aborting(changeEntry) 257 | shouter.shout("Accepted change %d/%d into working directory" % (amountofacceptedchanges, amountofchanges)) 258 | Commiter.addandcommit(changeEntry) 259 | return amountofacceptedchanges 260 | 261 | @staticmethod 262 | def collect_changes_to_accept_to_avoid_conflicts(changewhichcantbeacceptedalone, changes, maxchangesetstoaccepttogether): 263 | changestoaccept = [changewhichcantbeacceptedalone] 264 | nextchange = ImportHandler.getnextchangeset_fromsamecomponent(changewhichcantbeacceptedalone, changes) 265 | 266 | while True: 267 | if nextchange and len(changestoaccept) < maxchangesetstoaccepttogether: 268 | changestoaccept.append(nextchange) 269 | nextchange = ImportHandler.getnextchangeset_fromsamecomponent(nextchange, changes) 270 | else: 271 | break 272 | return changestoaccept 273 | 274 | def retryacceptincludingnextchangesets(self, change, changes): 275 | issuccessful = False 276 | changestoaccept = ImportHandler.collect_changes_to_accept_to_avoid_conflicts(change, changes, self.config.maxchangesetstoaccepttogether) 277 | amountofchangestoaccept = len(changestoaccept) 278 | 279 | if amountofchangestoaccept > 1: 280 | Changes.tostring(*changestoaccept) 281 | if self.config.useautomaticconflictresolution or self.is_user_agreeing_to_accept_next_change(change): 282 | shouter.shout("Trying to resolve conflict by accepting multiple changes") 283 | for index in range(1, amountofchangestoaccept): 284 | toaccept = changestoaccept[0:index + 1] # accept least possible amount of changes 285 | if Changes.accept(self.acceptlogpath, *toaccept): 286 | issuccessful = True 287 | break 288 | # ++++ check ++++ 289 | #else: 290 | # Changes.discard(*toaccept) # revert initial state 291 | if not issuccessful: 292 | self.is_user_aborting(change) 293 | 294 | @staticmethod 295 | def is_user_agreeing_to_accept_next_change(change): 296 | messagetoask = "Press Y for accepting following changes, press N to skip" 297 | while True: 298 | answer = input(messagetoask).lower() 299 | if answer == "y": 300 | return True 301 | elif answer == "n": 302 | return not ImportHandler.is_user_aborting(change) 303 | else: 304 | shouter.shout("Please answer with Y/N, input was " + answer) 305 | 306 | @staticmethod 307 | def is_user_aborting(change): 308 | shouter.shout("Last executed command: \n" + Changes.latest_accept_command) 309 | shouter.shout("Appropriate git commit command \n" + Commiter.getcommitcommand(change)) 310 | reallycontinue = "Do you want to continue? Y for continue, any key for abort" 311 | if input(reallycontinue).lower() == "y": 312 | return True 313 | else: 314 | sys.exit("Please check the output/log and rerun program with resume") 315 | 316 | @staticmethod 317 | def getnextchangeset_fromsamecomponent(currentchangeentry, changeentries): 318 | nextchangeentry = None 319 | component = currentchangeentry.component 320 | nextindex = changeentries.index(currentchangeentry) + 1 321 | while not nextchangeentry and nextindex < len(changeentries): 322 | candidateentry = changeentries[nextindex] 323 | if not candidateentry.isAccepted() and candidateentry.component == component: 324 | nextchangeentry = candidateentry 325 | nextindex += 1 326 | return nextchangeentry 327 | 328 | def getchangeentriesofstreamcomponents(self, componentbaselineentries): 329 | missingchangeentries = {} 330 | shouter.shout("Start collecting changeentries") 331 | for componentBaseLineEntry in componentbaselineentries: 332 | shouter.shout("Collect changes until baseline %s of component %s" % 333 | (componentBaseLineEntry.baselinename, componentBaseLineEntry.componentname)) 334 | changeentries = self.getchangeentriesofbaseline(componentBaseLineEntry.baseline) 335 | for changeentry in changeentries: 336 | missingchangeentries[changeentry.revision] = changeentry 337 | return missingchangeentries 338 | 339 | def readhistory(self, componentbaselineentries, streamname): 340 | if not self.config.useprovidedhistory: 341 | warning = "Warning - UseProvidedHistory is set to false, merge-conflicts are more likely to happen. \n " \ 342 | "For more information see https://github.com/rtcTo/rtc2git/wiki/Getting-your-History-Files" 343 | shouter.shout(warning) 344 | return None 345 | historyuuids = {} 346 | shouter.shout("Start reading history files") 347 | for componentBaseLineEntry in componentbaselineentries: 348 | history = self.gethistory(componentBaseLineEntry.componentname, streamname) 349 | historyuuids[componentBaseLineEntry.component] = history 350 | return historyuuids 351 | 352 | @staticmethod 353 | def getchangeentriestoaccept(missingchangeentries, history): 354 | changeentriestoaccept = [] 355 | if history: 356 | historywithchangeentryobject = {} 357 | for key in history.keys(): 358 | currentuuids = history.get(key) 359 | changeentries = [] 360 | for uuid in currentuuids: 361 | changeentry = missingchangeentries.get(uuid) 362 | if changeentry: 363 | changeentries.append(changeentry) 364 | historywithchangeentryobject[key] = changeentries 365 | changeentriestoaccept = sorter.tosortedlist(historywithchangeentryobject) 366 | else: 367 | changeentriestoaccept.extend(missingchangeentries.values()) 368 | # simple sort by date 369 | changeentriestoaccept.sort(key=lambda change: change.date) 370 | return changeentriestoaccept 371 | 372 | @staticmethod 373 | def getchangeentriesfromfile(outputfilename): 374 | informationseparator = "@@" 375 | numberofexpectedinformationseparators = 5 376 | changeentries = [] 377 | component="unknown" 378 | componentprefix = "Component (" 379 | 380 | with open(outputfilename, 'r', encoding=shell.encoding) as file: 381 | currentline = "" 382 | currentinformationpresent = 0 383 | for line in file: 384 | cleanedline = line.strip() 385 | if cleanedline: 386 | if cleanedline.startswith(componentprefix): 387 | length = len(componentprefix) 388 | component = cleanedline[length:cleanedline.index(")", length)] 389 | else: 390 | currentinformationpresent += cleanedline.count(informationseparator) 391 | if currentline: 392 | currentline += os.linesep 393 | currentline += cleanedline 394 | if currentinformationpresent >= numberofexpectedinformationseparators: 395 | splittedlines = currentline.split(informationseparator) 396 | revisionwithbrackets = splittedlines[0].strip() 397 | revision = revisionwithbrackets[1:-1] 398 | author = splittedlines[1].strip() 399 | email = splittedlines[2].strip() 400 | comment = splittedlines[3].strip() 401 | date = splittedlines[4].strip() 402 | 403 | changeentries.append(ChangeEntry(revision, author, email, date, comment, component)) 404 | 405 | currentinformationpresent = 0 406 | currentline = "" 407 | return changeentries 408 | 409 | @staticmethod 410 | def getsimplehistoryfromfile(outputfilename): 411 | revisions = [] 412 | if not os.path.isfile(outputfilename): 413 | shouter.shout("History file not found: " + outputfilename) 414 | shouter.shout("Skipping this part of history") 415 | return revisions 416 | 417 | with open(outputfilename, 'r', encoding=shell.encoding) as file: 418 | for line in file: 419 | revisions.append(line.strip()) 420 | revisions.reverse() # to begin by the oldest 421 | return revisions 422 | 423 | def getchangeentriesofbaseline(self, baselinetocompare): 424 | return self.getchangeentriesbytypeandvalue(CompareType.baseline, baselinetocompare) 425 | 426 | def getchangeentriesofstream(self, streamtocompare): 427 | shouter.shout("Start collecting changes since baseline creation") 428 | missingchangeentries = {} 429 | changeentries = self.getchangeentriesbytypeandvalue(CompareType.stream, streamtocompare) 430 | for changeentry in changeentries: 431 | missingchangeentries[changeentry.revision] = changeentry 432 | return missingchangeentries 433 | 434 | def getchangeentriesofworkspace(self, workspacetocompare): 435 | missingchangeentries = {} 436 | changeentries = self.getchangeentriesbytypeandvalue(CompareType.workspace, workspacetocompare) 437 | for changeentry in changeentries: 438 | missingchangeentries[changeentry.revision] = changeentry 439 | return missingchangeentries 440 | 441 | def getchangeentriesbytypeandvalue(self, comparetype, value): 442 | dateformat = "yyyy-MM-dd HH:mm:ss" 443 | outputfilename = self.config.getlogpath("Compare_" + comparetype.name + "_" + value + ".txt") 444 | comparecommand = "%s --show-alias n --show-uuid y compare ws %s %s %s -r %s -I swc -C @@{name}@@{email}@@ --flow-directions i -D @@\"%s\"@@" \ 445 | % (self.config.scmcommand, self.config.workspace, comparetype.name, value, self.config.repo, 446 | dateformat) 447 | shell.execute(comparecommand, outputfilename) 448 | return ImportHandler.getchangeentriesfromfile(outputfilename) 449 | 450 | def gethistory(self, componentname, streamname): 451 | outputfilename = self.config.gethistorypath("History_%s_%s.txt" % (componentname, streamname)) 452 | return ImportHandler.getsimplehistoryfromfile(outputfilename) 453 | 454 | 455 | class ChangeEntry: 456 | def __init__(self, revision, author, email, date, comment, component="unknown"): 457 | self.revision = revision 458 | self.author = author 459 | self.email = email 460 | self.date = date 461 | self.comment = comment 462 | self.component = component 463 | self.setUnaccepted() 464 | 465 | def getgitauthor(self): 466 | authorrepresentation = "%s <%s>" % (self.author, self.email) 467 | return shell.quote(authorrepresentation) 468 | 469 | def setAccepted(self): 470 | self.accepted = True 471 | 472 | def setUnaccepted(self): 473 | self.accepted = False 474 | 475 | def isAccepted(self): 476 | return self.accepted 477 | 478 | def tostring(self): 479 | return "%s (Date: %s, Author: %s, Revision: %s, Component: %s, Accepted: %s)" % \ 480 | (self.comment, self.date, self.author, self.revision, self.component, self.accepted) 481 | 482 | 483 | @unique 484 | class CompareType(Enum): 485 | baseline = 1 486 | stream = 2 487 | workspace = 3 488 | -------------------------------------------------------------------------------- /shell.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from subprocess import call, check_output, CalledProcessError 3 | 4 | import shouter 5 | 6 | logcommands = False 7 | encoding = None 8 | 9 | 10 | def execute(command, outputfile=None, openmode="w"): 11 | shout_command_to_log(command, outputfile) 12 | if not outputfile: 13 | return call(command, shell=True) 14 | else: 15 | with open(outputfile, openmode, encoding=encoding) as file: 16 | return call(command, stdout=file, shell=True) 17 | 18 | 19 | def getoutput(command, stripped=True): 20 | shout_command_to_log(command) 21 | try: 22 | outputasbytestring = check_output(command, shell=True) 23 | output = outputasbytestring.decode(sys.stdout.encoding).splitlines() 24 | except CalledProcessError as e: 25 | shouter.shout(e) 26 | output = "" 27 | if not stripped: 28 | return output 29 | else: 30 | lines = [] 31 | for line in output: 32 | strippedline = line.strip() 33 | if strippedline: 34 | lines.append(strippedline) 35 | return lines 36 | 37 | 38 | def quote(stringtoquote): 39 | stringtoquote = stringtoquote.replace('\"', "'") # replace " with ' 40 | quotedstring = '\"' + stringtoquote + '\"' 41 | return escapeShellVariableExpansion(quotedstring) 42 | 43 | 44 | def escapeShellVariableExpansion(comment): 45 | return comment.replace('$', '"\\$"') 46 | 47 | 48 | def shout_command_to_log(command, outputfile=None): 49 | if logcommands: 50 | logmessage = "Executed Command: " + quote(command) 51 | if outputfile: 52 | shouter.shout(logmessage + " --> " + outputfile) 53 | else: 54 | shouter.shout(logmessage) 55 | 56 | 57 | def setencoding(encodingtobeset): 58 | global encoding 59 | if encodingtobeset: 60 | encoding = encodingtobeset 61 | -------------------------------------------------------------------------------- /shouter.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | import sys 3 | 4 | 5 | def shout(messagetoshout): 6 | safeshout("%s - %s" % (gettimestamp(), messagetoshout)) 7 | 8 | 9 | def shoutwithdate(messagetoshout): 10 | safeshout("%s - %s" % (getdatetimestamp(), messagetoshout)) 11 | 12 | 13 | def safeshout(messagetoshout): 14 | print(messagetoshout.encode('utf8').decode(sys.stdout.encoding)) 15 | 16 | 17 | def gettimestamp(): 18 | return datetime.now().strftime('%X') 19 | 20 | 21 | def getdatetimestamp(): 22 | return datetime.now().strftime('%x %X') 23 | -------------------------------------------------------------------------------- /sorter.py: -------------------------------------------------------------------------------- 1 | def tosortedlist(changeentrymap): 2 | sortedlist = [] 3 | expectedlistsize = len(__aslist(changeentrymap)) 4 | 5 | while len(sortedlist) < expectedlistsize: 6 | firstentryfromeachkey = __getfirstentryfromeachkeyasmap(changeentrymap) 7 | changesetwithearliestdate = __getchangeentrywithearliestdate(firstentryfromeachkey) 8 | __deleteentry(changeentrymap, changesetwithearliestdate) 9 | sortedlist.append(changesetwithearliestdate) 10 | 11 | return sortedlist 12 | 13 | 14 | def __getfirstentryfromeachkeyasmap(changeentrymap): 15 | firstentries = {} 16 | for key in changeentrymap.keys(): 17 | changeentries = changeentrymap.get(key) 18 | if changeentries: 19 | firstentries[key] = changeentries[0] 20 | return firstentries 21 | 22 | 23 | def __deleteentry(changeentrymap, changeentrytodelete): 24 | for key in changeentrymap.keys(): 25 | changeentries = changeentrymap.get(key) 26 | if changeentries and changeentrytodelete.revision is changeentries[0].revision: 27 | changeentries.remove(changeentrytodelete) 28 | break 29 | 30 | 31 | def __getchangeentrywithearliestdate(changeentries): 32 | changeentrywithearliestdate = None 33 | for key in changeentries.keys(): 34 | changeentry = changeentries.get(key) 35 | if not changeentrywithearliestdate or changeentry.date < changeentrywithearliestdate.date: 36 | changeentrywithearliestdate = changeentry 37 | return changeentrywithearliestdate 38 | 39 | 40 | def __aslist(anymap): 41 | resultlist = [] 42 | for key in anymap.keys(): 43 | for changeentry in anymap.get(key): 44 | resultlist.append(changeentry) 45 | return resultlist -------------------------------------------------------------------------------- /tests/resources/test_.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.class 3 | *.so 4 | *.pyc 5 | 6 | /*.suo 7 | /.classpath 8 | /.idea 9 | /.project 10 | /.settings 11 | /bin 12 | /dist 13 | /a?c 14 | -------------------------------------------------------------------------------- /tests/resources/test_.jazzignore: -------------------------------------------------------------------------------- 1 | ### Jazz Ignore 0 2 | # Ignored files and folders will not be committed, but may be modified during 3 | # accept or update. 4 | # - Ignore properties should contain a space separated list of filename patterns. 5 | # - Each pattern is case sensitive and surrounded by braces ('{' and '}'). 6 | # - "*" matches zero or more characters. 7 | # - "?" matches a single character. 8 | # - The pattern list may be split across lines by ending the line with a 9 | # backslash and starting the next line with a tab. 10 | # - Patterns in core.ignore prevent matching resources in the same 11 | # directory from being committed. 12 | # - Patterns in core.ignore.recursive matching resources in the current 13 | # directory and all subdirectories from being committed. 14 | # - The default value of core.ignore.recursive is *.class 15 | # - The default value for core.ignore is bin 16 | # 17 | # To ignore shell scripts and hidden files in this subtree: 18 | # e.g: core.ignore.recursive = {*.sh} {\.*} 19 | # 20 | # To ignore resources named 'bin' in the current directory (but allow 21 | # them in any sub directorybelow): 22 | # e.g: core.ignore.recursive = {*.sh} {\.*} 23 | # 24 | # NOTE: modifying ignore files will not change the ignore status of 25 | # Eclipse derived resources. 26 | 27 | core.ignore.recursive = {*.class} {*.so} \ 28 | {*.pyc} 29 | 30 | core.ignore = {*.suo} {.classpath} \ 31 | {.idea} \ 32 | {.project} \ 33 | {.settings} \ 34 | {bin} \ 35 | {dist} \ 36 | {a?c} 37 | 38 | -------------------------------------------------------------------------------- /tests/resources/test_config.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | Repo=https://rtc.supercompany.com/ccm/ 3 | User=superuser 4 | Password=supersecret 5 | GIT-Reponame = super.git 6 | WorkspaceName=Superworkspace 7 | Directory = /tmp/migration 8 | useExistingWorkspace = True 9 | ScmCommand = scm 10 | encoding = UTF-8 11 | RTCVersion = 6 12 | 13 | [Migration] 14 | StreamToMigrate = Superstream 15 | PreviousStream = Previousstream 16 | InitialBaseLines = Component1=Baseline1, Component2=Baseline2 17 | UseProvidedHistory = True 18 | UseAutomaticConflictResolution = True 19 | MaxChangeSetsToAcceptTogether = 100 20 | CommitMessageWorkItemPrefix = UP- 21 | Gitattributes = # Handle line endings automatically for text files; # and leave binary files untouched; * text=auto; *.sql text 22 | 23 | [Miscellaneous] 24 | LogShellCommands = True 25 | IgnoreFileExtensions = .zip; .jar 26 | IgnoreDirectories = projectX/WebContent/node_modules; projectY/distribution 27 | IncludeComponentRoots = True 28 | -------------------------------------------------------------------------------- /tests/resources/test_ignore_git_status_z.txt: -------------------------------------------------------------------------------- 1 | A project1/src/tobedeleted.txtR project2/src/taka.txtproject1/src/taka.txtR project2/src/takatuka.txtproject2/src/tuka.txt?? project1/src/sub/kling -- klong.zip?? project1/src/sub/kling :and: klong.zip?? project1/src/sub/kling ;and; klong.zip?? project1/src/sub/kling >and< klong.zip?? project1/src/sub/kling \and\ klong.zip?? project1/src/sub/kling |and| klong.zip?? project1/src/sub/klingklong.zip D project1/src/sub/.jazzignore D project1/src/.gitignore D project1/src/sub/.gitignore -------------------------------------------------------------------------------- /tests/resources/test_minimum_config.ini: -------------------------------------------------------------------------------- 1 | [General] 2 | Repo = https://rtc.minicompany.com/ccm/ 3 | User = miniuser 4 | Password = minisecret 5 | GIT-Reponame = mini.git 6 | WorkspaceName = Miniworkspace 7 | 8 | [Migration] 9 | StreamToMigrate = Ministream 10 | -------------------------------------------------------------------------------- /tests/resources/test_rtcFunctions_SampleCompareOutputInUtf8.txt: -------------------------------------------------------------------------------- 1 | Component (_2mytestcomponent2-UUID) "TestComponent2" 2 | (_RnnFo8QyEeGBKPBcKV85Sw) @@John ÆØÅ@@Jon.Doe@rtc2git.rocks@@Comment@@2015-05-26 10:40:00@@ 3 | -------------------------------------------------------------------------------- /tests/resources/test_rtcFunctions_SampleCompareOutputWithComponents.txt: -------------------------------------------------------------------------------- 1 | Component (_2mytestcomponent2-UUID) "TestComponent2" 2 | (_uuID-1) @@Bubba Gump@@bubba.gump@shrimps.com@@ 1234: work item - commit 1 @@2015-06-07 16:34:22@@ 3 | (_uuID-3) @@Bubba Gump@@bubba.gump@shrimps.com@@ 1234: work item - commit 3 @@2015-08-25 16:15:50@@ 4 | Component (_3mytestcomponent3-UUID) "TestComponent3" 5 | (_uuID-2) @@Bubba Gump@@bubba.gump@shrimps.com@@ 1234: work item - commit 2 @@2015-06-08 16:34:22@@ 6 | (_uuID-4) @@Bubba Gump@@bubba.gump@shrimps.com@@ 1234: work item - commit 4 @@2015-08-26 16:15:50@@ 7 | Component (_4mytestcomponent4-UUID) "TestComponent4" 8 | -------------------------------------------------------------------------------- /tests/resources/test_rtcFunctions_SampleCompareOutputWithLineBreaks.txt: -------------------------------------------------------------------------------- 1 | Component (_2mytestcomponent2-UUID) "TestComponent2" 2 | (someUUID)@@Jon Doe@@Jon.Doe@rtc2git.rocks@@My first commit in rtc! :D@@2015-05-26 10:40:00@@ 3 | 4 | Component (_3mytestcomponent3-UUID) "TestComponent3" 5 | (someotherUUID)@@Jon Doe@@Jon.Doe@rtc2git.rocks@@I want to commit on my flight to Riga :( 6 | This is a new line@@2015-05-26 10:42:00@@ -------------------------------------------------------------------------------- /tests/resources/test_rtcFunctions_SampleCompareOutputWithoutLineBreaks.txt: -------------------------------------------------------------------------------- 1 | Component (_2mytestcomponent2-UUID) "TestComponent2" 2 | (someUUID)@@Jon Doe@@Jon.Doe@rtc2git.rocks@@My first commit in rtc! :D@@2015-05-26 10:40:00@@ 3 | (someotherUUID)@@Jon Doe@@Jon.Doe@rtc2git.rocks@@I want to commit on my flight to Riga :(@@2015-05-26 10:42:00@@ -------------------------------------------------------------------------------- /tests/test_configuration.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import configuration 5 | import shell 6 | from configuration import Builder 7 | from tests import testhelper 8 | 9 | 10 | class ConfigurationTestCase(unittest.TestCase): 11 | 12 | def setUp(self): 13 | self.workdirectory = os.path.dirname(os.path.realpath(__file__)) 14 | # reset global shell variables 15 | shell.logcommands = False 16 | shell.encoding = None 17 | configuration.setconfigfile(None) 18 | configuration.setUser(None) 19 | configuration.setPassword(None) 20 | 21 | def test_DeletionOfFolder(self): 22 | config = Builder().setworkdirectory(self.workdirectory).build() 23 | samplepath = os.path.dirname(config.getlogpath("anyPath")) 24 | self.assertTrue(os.path.exists(samplepath)) 25 | config.deletelogfolder() 26 | self.assertFalse(os.path.exists(samplepath)) 27 | 28 | def test_ReaddingLogFolderAfterDeletion(self): 29 | config = Builder().setworkdirectory(self.workdirectory).build() 30 | samplepath = os.path.dirname(config.getlogpath("anyPath")) 31 | self.assertTrue(os.path.exists(samplepath)) 32 | config.deletelogfolder() 33 | self.assertFalse(os.path.exists(samplepath)) 34 | samplepath = os.path.dirname(config.getlogpath("anyPath")) 35 | self.assertTrue(os.path.exists(samplepath)) 36 | 37 | def test_sampleBoolConfigEntrySetToFalse_ShouldBeFalse(self): 38 | config = Builder().setuseautomaticconflictresolution("False").build() 39 | self.assertFalse(config.useautomaticconflictresolution) 40 | 41 | def test_sampleBoolConfigEntrySetToTrue_ShouldBeTrue(self): 42 | config = Builder().setuseautomaticconflictresolution("True").build() 43 | self.assertTrue(config.useautomaticconflictresolution) 44 | 45 | def test_getSampleConfig_ExpectInitializedConfigWithDefaultValues(self): 46 | config = configuration.read(testhelper.getrelativefilename("../config.ini.sample")) 47 | self.assertEqual("lscm", config.scmcommand) 48 | self.assertEqual(config, configuration.get()) 49 | 50 | def test_fileExtensionsToBeIgnored_ShouldBeEmpty_FromNone(self): 51 | config = Builder().setignorefileextensions(configuration.parsesplittedproperty(None)).build() 52 | self.assertEqual(0, len(config.ignorefileextensions)) 53 | 54 | def test_fileExtensionsToBeIgnored_ShouldBeEmpty_FromEmpty(self): 55 | config = Builder().setignorefileextensions("").build() 56 | self.assertEqual(0, len(config.ignorefileextensions)) 57 | 58 | def test_fileExtensionsToBeIgnored_SingleExtension(self): 59 | config = Builder().setignorefileextensions(configuration.parsesplittedproperty(" .zip ")).build() 60 | self.assertEqual(1, len(config.ignorefileextensions)) 61 | self.assertEqual(['.zip'], config.ignorefileextensions) 62 | 63 | def test_fileExtensionsToBeIgnored_MultipleExtensions(self): 64 | config = Builder().setignorefileextensions(configuration.parsesplittedproperty(".zip; .jar; .exe")) \ 65 | .build() 66 | self.assertEqual(3, len(config.ignorefileextensions)) 67 | self.assertEqual(['.zip', '.jar', '.exe'], config.ignorefileextensions) 68 | 69 | def test_directoriesToBeIgnored_ShouldBeEmpty_FromNone(self): 70 | config = Builder().setignoredirectories(configuration.parsesplittedproperty(None)).build() 71 | self.assertEqual(0, len(config.ignoredirectories)) 72 | 73 | def test_directoriesToBeIgnored_ShouldBeEmpty_FromEmpty(self): 74 | config = Builder().setignoredirectories("").build() 75 | self.assertEqual(0, len(config.ignoredirectories)) 76 | 77 | def test_directoriesToBeIgnored_SingleExtension(self): 78 | config = Builder().setignoredirectories(configuration.parsesplittedproperty(" project/dist ")).build() 79 | self.assertEqual(1, len(config.ignoredirectories)) 80 | self.assertEqual(['project/dist'], config.ignoredirectories) 81 | 82 | def test_directoriesToBeIgnored_MultipleExtensions(self): 83 | config = Builder().setignoredirectories(configuration.parsesplittedproperty(" project/dist ; project/lib ; out ")) \ 84 | .build() 85 | self.assertEqual(3, len(config.ignoredirectories)) 86 | self.assertEqual(['project/dist', 'project/lib', 'out'], config.ignoredirectories) 87 | 88 | def test_gitattributes_ShouldBeEmpty_FromNone(self): 89 | config = Builder().setgitattributes(configuration.parsesplittedproperty(None)).build() 90 | self.assertEqual(0, len(config.gitattributes)) 91 | 92 | def test_gitattributes_ShouldBeEmpty_FromEmpty(self): 93 | config = Builder().setgitattributes(configuration.parsesplittedproperty("")).build() 94 | self.assertEqual(0, len(config.gitattributes)) 95 | 96 | def test_gitattributes__SingleProperty(self): 97 | config = Builder().setgitattributes(configuration.parsesplittedproperty(" * text=auto ")).build() 98 | self.assertEqual(1, len(config.gitattributes)) 99 | self.assertEqual(['* text=auto'], config.gitattributes) 100 | 101 | def test_gitattributes__MultipleProperties(self): 102 | config = Builder().setgitattributes(configuration.parsesplittedproperty(" # some comment ; * text=auto ; *.sql text ")).build() 103 | self.assertEqual(3, len(config.gitattributes)) 104 | self.assertEqual(['# some comment', '* text=auto', '*.sql text'], config.gitattributes) 105 | 106 | def test_read_passedin_configfile(self): 107 | self._assertTestConfig(configuration.read(testhelper.getrelativefilename('resources/test_config.ini'))) 108 | 109 | def test_read_passedin_configfile_expect_override_user_password(self): 110 | configuration.setUser('newUser') 111 | configuration.setPassword('newPassword') 112 | self._assertTestConfig(configuration.read(testhelper.getrelativefilename('resources/test_config.ini')), 113 | user='newUser', password='newPassword') 114 | 115 | def test_read_configfile_from_configuration(self): 116 | configuration.setconfigfile(testhelper.getrelativefilename('resources/test_config.ini')) 117 | self._assertTestConfig(configuration.read()) 118 | 119 | def test_read_minimumconfigfile_shouldrelyonfallbackvalues(self): 120 | configuration.setconfigfile(testhelper.getrelativefilename('resources/test_minimum_config.ini')) 121 | self._assertDefaultConfig(configuration.read()) 122 | 123 | def _assertTestConfig(self, config, user=None, password=None): 124 | # [General] 125 | self.assertEqual('https://rtc.supercompany.com/ccm/', config.repo) 126 | if not user: 127 | self.assertEqual('superuser', config.user) 128 | else: 129 | self.assertEqual(user, config.user) 130 | if not password: 131 | self.assertEqual('supersecret', config.password) 132 | else: 133 | self.assertEqual(password, config.password) 134 | self.assertEqual('super.git', config.gitRepoName) 135 | self.assertEqual('Superworkspace', config.workspace) 136 | self.assertEqual('/tmp/migration', config.workDirectory) 137 | self.assertTrue(config.useexistingworkspace) 138 | self.assertEqual('scm', config.scmcommand) 139 | self.assertEqual('UTF-8', shell.encoding) # directly deviated to shell 140 | self.assertEqual(6, config.rtcversion) 141 | # [Migration] 142 | self.assertEqual('Superstream', config.streamname) 143 | self.assertEqual('Previousstream', config.previousstreamname) 144 | initialcomponentbaselines = config.initialcomponentbaselines 145 | self.assertEqual(2, len(initialcomponentbaselines)) 146 | initialcomponentbaseline = initialcomponentbaselines[0] 147 | self.assertEqual('Component1', initialcomponentbaseline.componentname) 148 | self.assertEqual('Baseline1', initialcomponentbaseline.baselinename) 149 | initialcomponentbaseline = initialcomponentbaselines[1] 150 | self.assertEqual('Component2', initialcomponentbaseline.componentname) 151 | self.assertEqual('Baseline2', initialcomponentbaseline.baselinename) 152 | self.assertTrue(config.useprovidedhistory) 153 | self.assertTrue(config.useautomaticconflictresolution) 154 | self.assertEqual(100, config.maxchangesetstoaccepttogether) 155 | self.assertEqual("UP-", config.commitmessageprefix) 156 | gitattributes = config.gitattributes 157 | self.assertEqual(4, len(gitattributes)) 158 | self.assertEqual('# Handle line endings automatically for text files', gitattributes[0]) 159 | self.assertEqual('# and leave binary files untouched', gitattributes[1]) 160 | self.assertEqual('* text=auto', gitattributes[2]) 161 | self.assertEqual('*.sql text', gitattributes[3]) 162 | # [Miscellaneous] 163 | self.assertTrue(shell.logcommands) # directly deviated to shell 164 | ignorefileextensions = config.ignorefileextensions 165 | self.assertEqual(2, len(ignorefileextensions)) 166 | self.assertEqual('.zip', ignorefileextensions[0]) 167 | self.assertEqual('.jar', ignorefileextensions[1]) 168 | self.assertTrue(config.includecomponentroots) 169 | ignoredirectories = config.ignoredirectories 170 | self.assertEqual(2, len(ignoredirectories)) 171 | self.assertEqual('projectX/WebContent/node_modules', ignoredirectories[0]) 172 | self.assertEqual('projectY/distribution', ignoredirectories[1]) 173 | 174 | def _assertDefaultConfig(self, config): 175 | # [General] 176 | self.assertEqual('https://rtc.minicompany.com/ccm/', config.repo) 177 | self.assertEqual('miniuser', config.user) 178 | self.assertEqual('minisecret', config.password) 179 | self.assertEqual('mini.git', config.gitRepoName) 180 | self.assertEqual('Miniworkspace', config.workspace) 181 | self.assertEqual(os.getcwd(), config.workDirectory) 182 | self.assertFalse(config.useexistingworkspace) 183 | self.assertEqual('lscm', config.scmcommand) 184 | self.assertEqual(None, shell.encoding) # directly deviated to shell 185 | self.assertEqual(5, config.rtcversion) 186 | # [Migration] 187 | self.assertEqual('Ministream', config.streamname) 188 | self.assertEqual('', config.previousstreamname) 189 | self.assertEqual(0, len(config.initialcomponentbaselines)) 190 | self.assertFalse(config.useprovidedhistory) 191 | self.assertFalse(config.useautomaticconflictresolution) 192 | self.assertEqual(10, config.maxchangesetstoaccepttogether) 193 | self.assertEqual("", config.commitmessageprefix) 194 | self.assertEqual(0, len(config.gitattributes)) 195 | # [Miscellaneous] 196 | self.assertFalse(shell.logcommands) # directly deviated to shell 197 | self.assertEqual(0, len(config.ignorefileextensions)) 198 | self.assertFalse(config.includecomponentroots) 199 | -------------------------------------------------------------------------------- /tests/test_gitFunctions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import time 4 | from unittest.mock import patch, call 5 | import datetime 6 | 7 | import shell 8 | import configuration 9 | from gitFunctions import Commiter, Initializer 10 | from configuration import Builder 11 | from tests import testhelper 12 | 13 | 14 | class GitFunctionsTestCase(unittest.TestCase): 15 | 16 | def setUp(self): 17 | self.cwd = os.getcwd() 18 | configuration.config = Builder().build() 19 | 20 | def tearDown(self): 21 | configuration.config = None 22 | os.chdir(self.cwd) 23 | 24 | def test_ExistingFileStartsWithLowerCase_RenameToUpperCase_ExpectGitRename(self): 25 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 26 | originalfilename = "aFileWithLowerStart" 27 | newfilename = "AFileWithLowerStart" 28 | 29 | self.simulateCreationAndRenameInGitRepo(originalfilename, newfilename) 30 | self.assertGitStatusShowsIsRenamed() 31 | 32 | def assertGitStatusShowsIsRenamed(self): 33 | statusoutput = shell.getoutput("git status -z") 34 | modifier = statusoutput[0][0] 35 | self.assertEqual("R", modifier) 36 | 37 | def test_ExistingFileStartsWithUpperCase_RenameToLowerCase_ExpectGitRename(self): 38 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 39 | originalfilename = "AFileWithLowerStart" 40 | newfilename = "aFileWithLowerStart" 41 | 42 | self.simulateCreationAndRenameInGitRepo(originalfilename, newfilename) 43 | self.assertGitStatusShowsIsRenamed() 44 | 45 | def test_ExistingFileStartsWithUpperCaseInSubFolder_RenameToLowerCase_ExpectGitRename(self): 46 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 47 | originalfilename = "AFileWithLowerStart" 48 | newfilename = "aFileWithLowerStart" 49 | subfolder = "test" 50 | create_and_change_directory(subfolder) 51 | 52 | self.simulateCreationAndRenameInGitRepo(originalfilename, newfilename) 53 | self.assertGitStatusShowsIsRenamed() 54 | 55 | # test for issue #39 56 | def test_ExistingDirStartsWithUpperCaseA_RenameChildFile_ExpectGitRename(self): 57 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 58 | originalfilename = "AFileWithLowerStart" 59 | newfilename = "aFileWithLowerStart" 60 | subfolder = "Afolder" # this is key to reproduce #39 61 | create_and_change_directory(subfolder) 62 | 63 | self.simulateCreationAndRenameInGitRepo(originalfilename, newfilename) 64 | self.assertGitStatusShowsIsRenamed() 65 | 66 | def test_CreationOfGitIgnore_ExistAlready_ShouldntGetCreated(self): 67 | with testhelper.mkchdir("aFolder") as folder: 68 | configuration.config = Builder().setworkdirectory(folder).setgitreponame("test.git").build() 69 | ignore = '.gitignore' 70 | existing_git_ignore_entry = "test" 71 | Initializer().createrepo() 72 | with open(ignore, 'w') as gitIgnore: 73 | gitIgnore.write(existing_git_ignore_entry) 74 | Initializer.createignore() 75 | with open(ignore, 'r') as gitIgnore: 76 | for line in gitIgnore.readlines(): 77 | self.assertEqual(existing_git_ignore_entry, line) 78 | 79 | def test_CreationOfGitIgnore_DoesntExist_ShouldGetCreated(self): 80 | with testhelper.mkchdir("aFolder") as folder: 81 | configuration.config = Builder().setworkdirectory(folder).setgitreponame("test.git").build() 82 | ignore = '.gitignore' 83 | Initializer().createrepo() 84 | Initializer.createignore() 85 | gitignorepath = os.path.join(os.getcwd(), ignore) 86 | self.assertTrue(os.path.exists(gitignorepath)) 87 | 88 | def test_CreationOfGitIgnore_DoesntExist_ShouldGetCreated_WithDirectories(self): 89 | with testhelper.mkchdir("aFolder") as folder: 90 | configuration.config = Builder().setworkdirectory(folder).setgitreponame("test.git").setignoredirectories(["projectX/dist", "projectZ/out"]).build() 91 | ignore = '.gitignore' 92 | Initializer().createrepo() 93 | Initializer.createignore() 94 | gitignorepath = os.path.join(os.getcwd(), ignore) 95 | self.assertTrue(os.path.exists(gitignorepath)) 96 | expectedlines = [] 97 | expectedlines.append(".jazz5\n") 98 | expectedlines.append(".metadata\n") 99 | expectedlines.append(".jazzShed\n") 100 | expectedlines.append("\n") 101 | expectedlines.append("# directories\n") 102 | expectedlines.append("/projectX/dist\n") 103 | expectedlines.append("/projectZ/out\n") 104 | expectedlines.append("\n") 105 | with open(gitignorepath, 'r') as gitignore: 106 | lines = gitignore.readlines() 107 | self.assertEqual(expectedlines, lines) 108 | 109 | def test_CreationOfGitattributes_ExistAlready_ShouldntGetCreated(self): 110 | with testhelper.mkchdir("aFolder") as folder: 111 | configuration.config = Builder().setworkdirectory(folder).setgitreponame("test.git").setgitattributes(["# comment", "*.sql text"]).build() 112 | attributes = '.gitattributes' 113 | existing_git_attribute_entry = "* text=auto" 114 | Initializer().createrepo() 115 | with open(attributes, 'w') as gitattributes: 116 | gitattributes.write(existing_git_attribute_entry) 117 | Initializer.createattributes() 118 | with open(attributes, 'r') as gitattributes: 119 | for line in gitattributes.readlines(): 120 | self.assertEqual(existing_git_attribute_entry, line) 121 | 122 | def test_CreationOfGitattributes_DoesntExist_ShouldGetCreated(self): 123 | with testhelper.mkchdir("aFolder") as folder: 124 | configuration.config = Builder().setworkdirectory(folder).setgitreponame("test.git").setgitattributes(["# comment", "* text=auto"]).build() 125 | attributes = '.gitattributes' 126 | Initializer().createrepo() 127 | Initializer.createattributes() 128 | gitattributespath = os.path.join(os.getcwd(), attributes) 129 | self.assertTrue(os.path.exists(gitattributespath)) 130 | with open(gitattributespath, 'r') as gitattributes: 131 | lines = gitattributes.readlines() 132 | self.assertEqual(2, len(lines)) 133 | self.assertEquals('# comment\n', lines[0]) 134 | self.assertEquals('* text=auto\n', lines[1]) 135 | 136 | def test_BranchRenaming_TargetBranchDoesntExist(self): 137 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 138 | branchname = "hello" 139 | Commiter.branch(branchname) 140 | self.assertEqual(0, Commiter.promotebranchtomaster(branchname)) 141 | 142 | def test_BranchRenaming_TargetBranchExist_ShouldBeSuccessful(self): 143 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 144 | branchname = "hello" 145 | Commiter.branch(branchname) 146 | self.assertEqual(0, Commiter.promotebranchtomaster(branchname)) 147 | time.sleep(1) 148 | self.assertEqual(0, Commiter.promotebranchtomaster(branchname)) 149 | 150 | @patch('gitFunctions.datetime') 151 | def test_BranchRenaming_TwoCallsAtTheSameTime_ShouldFail(self, datetimemock): 152 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 153 | branchname = "hello" 154 | Commiter.branch(branchname) 155 | faketime = datetime.datetime(2015, 11, 11, 11, 11, 11) 156 | datetimemock.now.return_value = faketime 157 | self.assertEqual(0, Commiter.promotebranchtomaster(branchname)) 158 | self.assertEqual(1, Commiter.promotebranchtomaster(branchname)) 159 | 160 | def test_CopyBranch_TargetDoesntExist_ShouldBeSucessful(self): 161 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 162 | branchname = "hello" 163 | self.assertEqual(0, Commiter.copybranch("master", branchname)) 164 | 165 | def test_CopyBranch_TargetAlreadyExist_ShouldFail(self): 166 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 167 | branchname = "hello" 168 | Commiter.branch(branchname) 169 | self.assertFalse(Commiter.copybranch("master", branchname) is 0) 170 | 171 | def test_splitoutputofgitstatusz(self): 172 | with open(testhelper.getrelativefilename('./resources/test_ignore_git_status_z.txt'), 'r') as file: 173 | repositoryfiles = Commiter.splitoutputofgitstatusz(file.readlines()) 174 | self.assertEqual(15, len(repositoryfiles)) 175 | self.assertEqual('project1/src/tobedeleted.txt', repositoryfiles[0]) 176 | self.assertEqual('project2/src/taka.txt', repositoryfiles[1]) 177 | self.assertEqual('project1/src/taka.txt', repositoryfiles[2]) # rename continuation would bite here 178 | self.assertEqual('project2/src/takatuka.txt', repositoryfiles[3]) 179 | self.assertEqual('project2/src/tuka.txt', repositoryfiles[4]) 180 | self.assertEqual('project1/src/sub/kling -- klong.zip', repositoryfiles[5]) 181 | self.assertEqual('project1/src/sub/kling :and: klong.zip', repositoryfiles[6]) 182 | self.assertEqual('project1/src/sub/kling ;and; klong.zip', repositoryfiles[7]) 183 | self.assertEqual('project1/src/sub/kling >and< klong.zip', repositoryfiles[8]) 184 | self.assertEqual('project1/src/sub/kling \\and\\ klong.zip', repositoryfiles[9]) 185 | self.assertEqual('project1/src/sub/kling |and| klong.zip', repositoryfiles[10]) 186 | self.assertEqual('project1/src/sub/klingklong.zip', repositoryfiles[11]) 187 | self.assertEqual('project1/src/sub/.jazzignore', repositoryfiles[12]) 188 | self.assertEqual('project1/src/.gitignore', repositoryfiles[13]) 189 | self.assertEqual('project1/src/sub/.gitignore', repositoryfiles[14]) 190 | 191 | def test_splitoutputofgitstatusz_filterprefix_A(self): 192 | with open(testhelper.getrelativefilename('./resources/test_ignore_git_status_z.txt'), 'r') as file: 193 | repositoryfiles = Commiter.splitoutputofgitstatusz(file.readlines(), 'A ') 194 | self.assertEqual(1, len(repositoryfiles)) 195 | self.assertEqual('project1/src/tobedeleted.txt', repositoryfiles[0]) 196 | 197 | def test_splitoutputofgitstatusz_filterprefix_D(self): 198 | with open(testhelper.getrelativefilename('./resources/test_ignore_git_status_z.txt'), 'r') as file: 199 | repositoryfiles = Commiter.splitoutputofgitstatusz(file.readlines(), ' D ') 200 | self.assertEqual(3, len(repositoryfiles)) 201 | self.assertEqual('project1/src/sub/.jazzignore', repositoryfiles[0]) 202 | self.assertEqual('project1/src/.gitignore', repositoryfiles[1]) 203 | self.assertEqual('project1/src/sub/.gitignore', repositoryfiles[2]) 204 | 205 | def test_splitoutputofgitstatusz_filterprefix_double_question(self): 206 | with open(testhelper.getrelativefilename('./resources/test_ignore_git_status_z.txt'), 'r') as file: 207 | repositoryfiles = Commiter.splitoutputofgitstatusz(file.readlines(), '?? ') 208 | self.assertEqual(7, len(repositoryfiles)) 209 | self.assertEqual('project1/src/sub/kling -- klong.zip', repositoryfiles[0]) 210 | self.assertEqual('project1/src/sub/kling :and: klong.zip', repositoryfiles[1]) 211 | self.assertEqual('project1/src/sub/kling ;and; klong.zip', repositoryfiles[2]) 212 | self.assertEqual('project1/src/sub/kling >and< klong.zip', repositoryfiles[3]) 213 | self.assertEqual('project1/src/sub/kling \\and\\ klong.zip', repositoryfiles[4]) 214 | self.assertEqual('project1/src/sub/kling |and| klong.zip', repositoryfiles[5]) 215 | self.assertEqual('project1/src/sub/klingklong.zip', repositoryfiles[6]) 216 | 217 | @patch('gitFunctions.shell') 218 | def test_restore_shed_gitignore_with_sibling_jazzignore(self, shellmock): 219 | with open(testhelper.getrelativefilename('./resources/test_ignore_git_status_z.txt'), 'r') as file: 220 | with patch('os.path.exists', return_value=True): # answer inquries for sibling .jazzignore with True 221 | Commiter.restore_shed_gitignore(file.readlines()) 222 | calls = [call.execute('git checkout -- project1/src/.gitignore'), call.execute('git checkout -- project1/src/sub/.gitignore')] 223 | shellmock.assert_has_calls(calls) 224 | 225 | @patch('gitFunctions.shell') 226 | def test_restore_shed_gitignore_without_sibling_jazzignore(self, shellmock): 227 | with open(testhelper.getrelativefilename('./resources/test_ignore_git_status_z.txt'), 'r') as file: 228 | with patch('os.path.exists', return_value=True): # answer inquries for sibling .jazzignore with False 229 | Commiter.restore_shed_gitignore(file.readlines()) 230 | calls = [] # if there are no siblings, we are not allowed to checkout 231 | shellmock.assert_has_calls(calls) 232 | 233 | def test_handleignore_global_extensions(self): 234 | with testhelper.mkchdir("aFolder") as folder: 235 | # create test repo 236 | configuration.config = Builder().setworkdirectory(folder).setgitreponame("test.git").setignorefileextensions(".zip; .jar").build() 237 | ignore = ".gitignore" 238 | Initializer().createrepo() 239 | # simulate addition of .zip and .jar files 240 | zip = "test.zip" 241 | with open(zip, 'w') as testzip: 242 | testzip.write("test zip content") 243 | jar = "test.jar" 244 | with open(jar, 'w') as testjar: 245 | testjar.write("test jar content") 246 | # do the filtering 247 | Commiter.handleignore() 248 | # check output of .gitignore 249 | with open(ignore, 'r') as gitIgnore: 250 | lines = gitIgnore.readlines() 251 | self.assertEqual(2, len(lines)) 252 | lines.sort() 253 | # the ignore should not be recursive: 254 | self.assertEqual('/' + jar, lines[0].strip()) 255 | self.assertEqual('/' + zip, lines[1].strip()) 256 | 257 | def test_handleignore_local_jazzignore_expect_new_gitignore(self): 258 | with testhelper.mkchdir("aFolder") as folder: 259 | configuration.config = Builder().setworkdirectory(folder).setgitreponame("test.git").build() 260 | Initializer().createrepo() 261 | subfolder = "aSubFolder" 262 | os.mkdir(subfolder) 263 | jazzignore = subfolder + os.sep + ".jazzignore" 264 | with open(jazzignore, 'w') as testjazzignore: 265 | testjazzignore.write("# my ignores\n") 266 | testjazzignore.write("core.ignore = {*.pyc}") 267 | expectedlines = ["\n","/*.pyc\n"] 268 | gitignore = subfolder + os.sep + ".gitignore" 269 | self.assertFalse(os.path.exists(gitignore)) 270 | Commiter.handleignore() 271 | self.assertTrue(os.path.exists(gitignore)) 272 | gitignorelines = [] 273 | with open(gitignore, 'r') as localgitignore: 274 | gitignorelines = localgitignore.readlines() 275 | self.assertEqual(expectedlines, gitignorelines) 276 | 277 | def test_handleignore_local_jazzignore_expect_overwrite_gitignore(self): 278 | with testhelper.mkchdir("aFolder") as folder: 279 | configuration.config = Builder().setworkdirectory(folder).setgitreponame("test.git").build() 280 | Initializer().createrepo() 281 | subfolder = "aSubFolder" 282 | os.mkdir(subfolder) 283 | gitignore = subfolder + os.sep + ".gitignore" 284 | with open(gitignore, 'w') as localgitignore: 285 | localgitignore.write('\n') 286 | localgitignore.write("/*.pyc") 287 | jazzignore = subfolder + os.sep + ".jazzignore" 288 | with open(jazzignore, 'w') as testjazzignore: 289 | testjazzignore.write("# my ignores\n") 290 | testjazzignore.write("core.ignore = {*.class} {bin}") 291 | expectedlines = ["\n","/*.class\n","/bin\n"] 292 | Commiter.handleignore() 293 | self.assertTrue(os.path.exists(gitignore)) 294 | gitignorelines = [] 295 | with open(gitignore, 'r') as localgitignore: 296 | gitignorelines = localgitignore.readlines() 297 | self.assertEqual(expectedlines, gitignorelines) 298 | 299 | def test_handleignore_local_jazzignore_expect_empty_gitignore(self): 300 | with testhelper.mkchdir("aFolder") as folder: 301 | configuration.config = Builder().setworkdirectory(folder).setgitreponame("test.git").build() 302 | Initializer().createrepo() 303 | subfolder = "aSubFolder" 304 | os.mkdir(subfolder) 305 | gitignore = subfolder + os.sep + ".gitignore" 306 | with open(gitignore, 'w') as localgitignore: 307 | localgitignore.write('\n') 308 | localgitignore.write("/*.pyc") 309 | jazzignore = subfolder + os.sep + ".jazzignore" 310 | with open(jazzignore, 'w') as testjazzignore: 311 | testjazzignore.write("# my ignores are empty\n") 312 | Commiter.handleignore() 313 | self.assertTrue(os.path.exists(gitignore)) 314 | gitignorelines = [] 315 | with open(gitignore, 'r') as localgitignore: 316 | gitignorelines = localgitignore.readlines() 317 | self.assertEqual(0, len(gitignorelines)) 318 | 319 | def test_handleignore_local_jazzignore_expect_delete_gitignore(self): 320 | with testhelper.mkchdir("aFolder") as folder: 321 | # create a repository with a .jazzignore and .gitignore file 322 | configuration.config = Builder().setworkdirectory(folder).setgitreponame("test.git").build() 323 | Initializer().createrepo() 324 | subfolder = "aSubFolder" 325 | os.mkdir(subfolder) 326 | jazzignore = subfolder + os.sep + ".jazzignore" 327 | with open(jazzignore, 'w') as testjazzignore: 328 | testjazzignore.write("# my ignores\n") 329 | testjazzignore.write("core.ignore = {*.pyc}") 330 | Commiter.addandcommit(testhelper.createchangeentry(comment="Initial .jazzignore")) 331 | gitignore = subfolder + os.sep + ".gitignore" 332 | self.assertTrue(os.path.exists(gitignore)) 333 | # now remove .jazzignore 334 | os.remove(jazzignore) 335 | Commiter.handleignore() 336 | self.assertFalse(os.path.exists(gitignore)) 337 | 338 | def test_checkbranchname_expect_valid(self): 339 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 340 | self.assertEqual(True, Commiter.checkbranchname("master"), "master should be a valid branch name") 341 | 342 | def test_checkbranchname_quoted_expect_invalid(self): 343 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 344 | self.assertEqual(False, Commiter.checkbranchname("'master pflaster'"), 345 | "'master pflaster' should not be a valid branch name") 346 | 347 | def test_checkbranchname_unquoted_expect_invalid(self): 348 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 349 | self.assertEqual(False, Commiter.checkbranchname("master pflaster"), 350 | "master pflaster should not be a valid branch name") 351 | 352 | def test_getcommentwithprefix_enabled_commitisattached_shouldreturnwithprefix(self): 353 | prefix = "APREFIX-" 354 | configuration.config = Builder().setcommitmessageprefix(prefix) 355 | comment = "1337: Upgrade to Wildfly - A comment" 356 | expectedcomment = prefix + comment 357 | self.assertEqual(expectedcomment, Commiter.getcommentwithprefix(comment)) 358 | 359 | def test_getcommentwithprefix_enabled_commitisattached_containsspecialchars_shouldreturnwithprefix(self): 360 | prefix = "PR-" 361 | configuration.config = Builder().setcommitmessageprefix(prefix) 362 | comment = "1338: VAT: VAT-Conditions defined with 0 % and 0 amount - reverse" 363 | expectedcomment = prefix + comment 364 | self.assertEqual(expectedcomment, Commiter.getcommentwithprefix(comment)) 365 | 366 | def test_getcommentwithprefix_disabled_commitisattached_shouldreturncommentwithoutprefix(self): 367 | configuration.config = Builder().setcommitmessageprefix("") 368 | comment = "1337: Upgrade to Wildfly - A comment" 369 | self.assertEqual(comment, Commiter.getcommentwithprefix(comment)) 370 | 371 | def test_getcommentwithprefix_enabled_commitisnotattachedtoanworkitem_shouldreturncommentwithoutprefix(self): 372 | configuration.config = Builder().setcommitmessageprefix("PR-") 373 | comment = "US1337: Fix some problems" 374 | self.assertEqual(comment, Commiter.getcommentwithprefix(comment)) 375 | 376 | def test_IllegalGitCharsShouldntCreateFile_Arrow(self): 377 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 378 | Commiter.addandcommit(testhelper.createchangeentry(comment="Some commit -> Bad")) 379 | self.assertEqual(0, len(shell.getoutput("git status -z")), "No file should be created by commit message") 380 | 381 | def test_IllegalGitCharsShouldntCreateFile_LongArrow(self): 382 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 383 | Commiter.addandcommit(testhelper.createchangeentry(comment="Some commit --> Worse")) 384 | self.assertEqual(0, len(shell.getoutput("git status -z")), "No file should be created by commit message") 385 | 386 | def test_IllegalGitCharsShouldntCreateFile_NoCommentCase(self): 387 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 388 | Commiter.addandcommit(testhelper.createchangeentry(comment="")) 389 | self.assertEqual(0, len(shell.getoutput("git status -z")), "No file should be created by commit message") 390 | 391 | def test_IllegalGitCharsShouldntCreateFile_SpecialCaseAlreadyQuoted(self): 392 | with testhelper.createrepo(folderprefix="gitfunctionstestcase_"): 393 | Commiter.addandcommit(testhelper.createchangeentry(comment="Check out \"" + ">" + "\"US3333\"")) 394 | self.assertEqual(0, len(shell.getoutput("git status -z")), "No file should be created by commit message") 395 | 396 | def test_translatejazzignore(self): 397 | with open(testhelper.getrelativefilename('./resources/test_.jazzignore'), 'r') as jazzignore: 398 | inputlines = jazzignore.readlines() 399 | with open(testhelper.getrelativefilename('./resources/test_.gitignore'), 'r') as gitignore: 400 | expectedlines = gitignore.readlines() 401 | self.assertEqual(expectedlines, Commiter.translatejazzignore(inputlines)) 402 | 403 | def testDefaultemail(self): 404 | self.assertEqual("default@rtc.to", Commiter.defaultemail(None)) 405 | self.assertEqual("default@rtc.to", Commiter.defaultemail("")) 406 | self.assertEqual("_@rtc.to", Commiter.defaultemail(".")) 407 | self.assertEqual("_.@rtc.to", Commiter.defaultemail("..")) 408 | self.assertEqual("_._@rtc.to", Commiter.defaultemail("...")) 409 | self.assertEqual("_@rtc.to", Commiter.defaultemail(" ")) 410 | self.assertEqual("_.@rtc.to", Commiter.defaultemail(" ")) 411 | self.assertEqual("_._@rtc.to", Commiter.defaultemail(" ")) 412 | self.assertEqual("a@rtc.to", Commiter.defaultemail("A")) 413 | self.assertEqual("a.@rtc.to", Commiter.defaultemail("a ")) 414 | self.assertEqual("ab@rtc.to", Commiter.defaultemail("aB")) 415 | self.assertEqual("a.b@rtc.to", Commiter.defaultemail("A b")) 416 | self.assertEqual("a.b@rtc.to", Commiter.defaultemail("A#B")) 417 | self.assertEqual("scarlet.o_hara@rtc.to", Commiter.defaultemail("Scarlet O'Hara")) 418 | 419 | @patch('shell.call') 420 | def testReplaceAuthor(self, mock_call): 421 | expected_mail_replace_command = "git config --replace-all user.email myEmail" 422 | Commiter.replaceauthor("myAuthor@do.not.check", "myEmail") 423 | self.assertEqual(2, mock_call.call_count) 424 | mock_call.assert_any_call(expected_mail_replace_command, shell=True) 425 | 426 | @patch('shell.call') 427 | def testReplaceAuthor_emptyEmail_shouldBeReplacedWithDefault(self, mock_call): 428 | expected_mail_replace_command = "git config --replace-all user.email my.author@rtc.to" 429 | Commiter.replaceauthor("My Author", "") 430 | self.assertEqual(2, mock_call.call_count) 431 | mock_call.assert_any_call(expected_mail_replace_command, shell=True) 432 | 433 | def simulateCreationAndRenameInGitRepo(self, originalfilename, newfilename): 434 | open(originalfilename, 'a').close() # create file 435 | Initializer.initialcommit() 436 | Commiter.pushmaster() 437 | os.rename(originalfilename, newfilename) # change capitalization 438 | shell.execute("git add -A") 439 | Commiter.handle_captitalization_filename_changes() 440 | 441 | 442 | def create_and_change_directory(subfolder): 443 | os.mkdir(subfolder) 444 | os.chdir(subfolder) 445 | 446 | 447 | if __name__ == '__main__': 448 | unittest.main() 449 | -------------------------------------------------------------------------------- /tests/test_migration.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import sys 4 | from unittest.mock import patch 5 | 6 | import migration 7 | from configuration import Builder 8 | import configuration 9 | from tests import testhelper 10 | 11 | 12 | class MigrationTestCase(unittest.TestCase): 13 | def setUp(self): 14 | self.rootfolder = os.path.dirname(os.path.realpath(__file__)) 15 | 16 | @patch('migration.Commiter') 17 | @patch('migration.Initializer') 18 | @patch('migration.RTCInitializer') 19 | @patch('migration.os') 20 | @patch('configuration.shutil') 21 | def testDeletionOfLogFolderOnInitalization(self, shutil_mock, os_mock, rtc_initializer_mock, git_initializer_mock, 22 | git_comitter_mock): 23 | config = Builder().setrootfolder(self.rootfolder).build() 24 | anylogpath = config.getlogpath("testDeletionOfLogFolderOnInitalization") 25 | os_mock.path.exists.return_value = False 26 | configuration.config = config 27 | 28 | migration.initialize() 29 | 30 | expectedlogfolder = self.rootfolder + os.sep + "Logs" 31 | shutil_mock.rmtree.assert_called_once_with(expectedlogfolder) 32 | 33 | def testExistRepo_Exists_ShouldReturnTrue(self): 34 | with testhelper.createrepo(folderprefix="test_migration"): 35 | self.assertTrue(migration.existsrepo()) 36 | 37 | def testExistRepo_DoesntExist_ShouldReturnFalse(self): 38 | configuration.config = Builder().setworkdirectory(self.rootfolder).setgitreponame("test.git").build() 39 | self.assertFalse(migration.existsrepo()) 40 | 41 | def testParseCommandLine_expect_specified_configfile_shortoption(self): 42 | configfilename = 'myShortTestConfig.ini' 43 | sys.argv = ['migration.py', '-c', configfilename] 44 | migration.parsecommandline() 45 | self.assertEqual(configfilename, configuration.configfile) 46 | 47 | def testParseCommandLine_expect_specified_configfile_longoption(self): 48 | configfilename = 'myLongTestConfig.ini' 49 | sys.argv = ['migration.py', '--configfile', configfilename] 50 | migration.parsecommandline() 51 | self.assertEqual(configfilename, configuration.configfile) 52 | 53 | def testParseCommandLine_expect_default_configfile(self): 54 | sys.argv = ['migration.py'] 55 | migration.parsecommandline() 56 | self.assertEqual('config.ini', configuration.configfile) 57 | 58 | def testParseCommandLine_expect_specified_user_password_shortoptions(self): 59 | sys.argv = ['migration.py', '-u', 'anyUser', '-p', 'anyPassword' ] 60 | migration.parsecommandline() 61 | self.assertEqual('anyUser', configuration.user) 62 | self.assertEqual('anyPassword', configuration.password) 63 | 64 | def testParseCommandLine_expect_specified_user_password_longoptions(self): 65 | sys.argv = ['migration.py', '--user', 'anyUser', '--password', 'anyPassword' ] 66 | migration.parsecommandline() 67 | self.assertEqual('anyUser', configuration.user) 68 | self.assertEqual('anyPassword', configuration.password) 69 | 70 | def testParseCommandLine_expect_no_user_password(self): 71 | sys.argv = ['migration.py'] 72 | migration.parsecommandline() 73 | self.assertIsNone(configuration.user) 74 | self.assertIsNone(configuration.password) 75 | -------------------------------------------------------------------------------- /tests/test_rtcFunctions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | from unittest.mock import patch, call 4 | 5 | import configuration 6 | import shell 7 | from rtcFunctions import Changes, ChangeEntry, ImportHandler, WorkspaceHandler, CompareType 8 | from configuration import Builder 9 | from tests import testhelper 10 | 11 | __author__ = 'Manuel' 12 | 13 | 14 | class RtcFunctionsTestCase(unittest.TestCase): 15 | def setUp(self): 16 | self.workspace = "anyWorkspace" 17 | self.apath = "aLogPath" 18 | self.configBuilder = Builder() 19 | 20 | @patch('rtcFunctions.shell') 21 | def test_Accept_AssertThatCorrectParamaterGetPassedToShell(self, shell_mock): 22 | revision1 = "anyRevision" 23 | revision2 = "anyOtherRevision" 24 | anyurl = "anyUrl" 25 | config = self.configBuilder.setrepourl(anyurl).setworkspace(self.workspace).build() 26 | configuration.config = config 27 | Changes.accept(self.apath, testhelper.createchangeentry(revision1), testhelper.createchangeentry(revision2)) 28 | commandtemplate = u"lscm accept --verbose --overwrite-uncommitted --accept-missing-changesets --no-merge --repository-uri {0:s} --target {1:s} --changes {2:s} {3:s}" 29 | expected_accept_command = commandtemplate.format(anyurl, self.workspace, revision1, revision2) 30 | appendlogfileshortcut = "a" 31 | shell_mock.execute.assert_called_once_with(expected_accept_command, self.apath, appendlogfileshortcut) 32 | self.assertEqual(expected_accept_command, Changes.latest_accept_command) 33 | 34 | def test_Accept_AssertThatChangeEntriesGetAccepted(self): 35 | with patch.object(shell, 'execute', return_value=0) as shell_mock: 36 | revision1 = "anyRevision" 37 | revision2 = "anyOtherRevision" 38 | anyurl = "anyUrl" 39 | config = self.configBuilder.setrepourl(anyurl).setworkspace(self.workspace).build() 40 | configuration.config = config 41 | changeentry1 = testhelper.createchangeentry(revision1) 42 | changeentry2 = testhelper.createchangeentry(revision2) 43 | Changes.accept(self.apath, changeentry1, changeentry2) 44 | self.assertTrue(changeentry1.isAccepted()) 45 | self.assertTrue(changeentry1.isAccepted()) 46 | 47 | @patch('rtcFunctions.shell') 48 | def test_Discard_AssertThatCorrectParamaterGetPassedToShell(self, shell_mock): 49 | revision1 = "anyRevision" 50 | revision2 = "anyOtherRevision" 51 | anyurl = "anyUrl" 52 | config = self.configBuilder.setrepourl(anyurl).setworkspace(self.workspace).build() 53 | configuration.config = config 54 | Changes.discard(testhelper.createchangeentry(revision1), testhelper.createchangeentry(revision2)) 55 | expected_discard_command = "lscm discard -w %s -r %s -o %s %s" % (self.workspace, anyurl, revision1, revision2) 56 | shell_mock.execute.assert_called_once_with(expected_discard_command) 57 | 58 | def test_Discard_AssertThatChangeEntriesGetUnAccepted(self): 59 | with patch.object(shell, 'execute', return_value=0) as shell_mock: 60 | revision1 = "anyRevision" 61 | revision2 = "anyOtherRevision" 62 | anyurl = "anyUrl" 63 | config = self.configBuilder.setrepourl(anyurl).setworkspace(self.workspace).build() 64 | configuration.config = config 65 | changeentry1 = testhelper.createchangeentry(revision1) 66 | changeentry1.setAccepted() 67 | changeentry2 = testhelper.createchangeentry(revision2) 68 | changeentry2.setAccepted() 69 | Changes.discard(changeentry1, changeentry2) 70 | self.assertFalse(changeentry1.isAccepted()) 71 | self.assertFalse(changeentry2.isAccepted()) 72 | 73 | def test_ReadChangesetInformationFromFile_WithoutLineBreakInComment_ShouldBeSuccessful(self): 74 | sample_file_path = self.get_Sample_File_Path("SampleCompareOutputWithoutLineBreaks.txt") 75 | changeentries = ImportHandler.getchangeentriesfromfile(sample_file_path) 76 | self.assertEqual(2, len(changeentries)) 77 | author = "Jon Doe" 78 | mail = "Jon.Doe@rtc2git.rocks" 79 | self.assert_Change_Entry(changeentries[0], author, mail, "My first commit in rtc! :D", "2015-05-26 10:40:00", "_2mytestcomponent2-UUID") 80 | self.assert_Change_Entry(changeentries[1], author, mail, "I want to commit on my flight to Riga :(", 81 | "2015-05-26 10:42:00", "_2mytestcomponent2-UUID") 82 | 83 | def test_ReadChangesetInformationFromFile_WithMultipleComponents(self): 84 | sample_file_path = self.get_Sample_File_Path("SampleCompareOutputWithComponents.txt") 85 | changeentries = ImportHandler.getchangeentriesfromfile(sample_file_path) 86 | self.assertEqual(4, len(changeentries)) 87 | author = "Bubba Gump" 88 | mail = "bubba.gump@shrimps.com" 89 | self.assert_Change_Entry(changeentries[0], author, mail, "1234: work item - commit 1", "2015-06-07 16:34:22", "_2mytestcomponent2-UUID") 90 | self.assert_Change_Entry(changeentries[1], author, mail, "1234: work item - commit 3", "2015-08-25 16:15:50", "_2mytestcomponent2-UUID") 91 | self.assert_Change_Entry(changeentries[2], author, mail, "1234: work item - commit 2", "2015-06-08 16:34:22", "_3mytestcomponent3-UUID") 92 | self.assert_Change_Entry(changeentries[3], author, mail, "1234: work item - commit 4", "2015-08-26 16:15:50", "_3mytestcomponent3-UUID") 93 | 94 | def test_ReadChangesetInformationFromFile_WithLineBreakInComment_ShouldBeSuccesful(self): 95 | sample_file_path = self.get_Sample_File_Path("SampleCompareOutputWithLineBreaks.txt") 96 | changeentries = ImportHandler.getchangeentriesfromfile(sample_file_path) 97 | self.assertEqual(2, len(changeentries)) 98 | author = "Jon Doe" 99 | mail = "Jon.Doe@rtc2git.rocks" 100 | self.assert_Change_Entry(changeentries[0], author, mail, "My first commit in rtc! :D", "2015-05-26 10:40:00", "_2mytestcomponent2-UUID") 101 | expectedcomment = "I want to commit on my flight to Riga :(" + os.linesep + "This is a new line" 102 | self.assert_Change_Entry(changeentries[1], author, mail, expectedcomment, "2015-05-26 10:42:00", "_3mytestcomponent3-UUID") 103 | 104 | def test_ReadChangesetInformationFromFile_InUtf8_ShouldBeSuccesful(self): 105 | shell.setencoding("UTF-8") 106 | sample_file_path = self.get_Sample_File_Path("SampleCompareOutputInUtf8.txt") 107 | changeentries = ImportHandler.getchangeentriesfromfile(sample_file_path) 108 | self.assertEqual(1, len(changeentries)) 109 | author = "John ÆØÅ" 110 | mail = "Jon.Doe@rtc2git.rocks" 111 | self.assert_Change_Entry(changeentries[0], author, mail, "Comment", "2015-05-26 10:40:00", "_2mytestcomponent2-UUID") 112 | 113 | @patch('rtcFunctions.shell') 114 | @patch('builtins.input', return_value='') 115 | def test_RetryAccept_AssertThatTwoChangesGetAcceptedTogether(self, inputmock, shellmock): 116 | changeentry1 = testhelper.createchangeentry("anyRevId") 117 | changeentry2 = testhelper.createchangeentry("anyOtherRevId") 118 | changeentries = [changeentry1, changeentry2] 119 | 120 | shellmock.execute.return_value = 0 121 | self.configBuilder.setrepourl("anyurl").setuseautomaticconflictresolution("True").setmaxchangesetstoaccepttogether(10).setworkspace("anyWs") 122 | config = self.configBuilder.build() 123 | configuration.config = config 124 | 125 | handler = ImportHandler() 126 | handler.retryacceptincludingnextchangesets(changeentry1, changeentries) 127 | 128 | expectedshellcommand = 'lscm accept --verbose --overwrite-uncommitted --accept-missing-changesets --no-merge --repository-uri anyurl --target anyWs --changes anyRevId anyOtherRevId' 129 | shellmock.execute.assert_called_once_with(expectedshellcommand, handler.config.getlogpath("accept.txt"), "a") 130 | 131 | @patch('rtcFunctions.shell') 132 | @patch('builtins.input', return_value='') 133 | def test_RetryAccept_AssertThatOnlyChangesFromSameComponentGetAcceptedTogether(self, inputmock, shellmock): 134 | component1 = "uuid1" 135 | component2 = "uuid2" 136 | changeentry1 = testhelper.createchangeentry(revision="anyRevId", component=component1) 137 | changeentry2 = testhelper.createchangeentry(revision="component2RevId", component=component2) 138 | changeentry3 = testhelper.createchangeentry(revision="anyOtherRevId", component=component1) 139 | changeentries = [changeentry1, changeentry2, changeentry3] 140 | 141 | shellmock.execute.return_value = 0 142 | self.configBuilder.setrepourl("anyurl").setuseautomaticconflictresolution("True").setmaxchangesetstoaccepttogether(10).setworkspace("anyWs") 143 | config = self.configBuilder.build() 144 | configuration.config = config 145 | 146 | handler = ImportHandler() 147 | handler.retryacceptincludingnextchangesets(changeentry1, changeentries) 148 | 149 | expectedshellcommand = 'lscm accept --verbose --overwrite-uncommitted --accept-missing-changesets --no-merge --repository-uri anyurl --target anyWs --changes anyRevId anyOtherRevId' 150 | shellmock.execute.assert_called_once_with(expectedshellcommand, handler.config.getlogpath("accept.txt"), "a") 151 | 152 | @patch('rtcFunctions.shell') 153 | @patch('builtins.input', return_value='N') 154 | @patch('sys.exit') 155 | def test_RetryAccept_NotSuccessful_AndExit(self, exitmock, inputmock, shellmock): 156 | component1 = "uuid1" 157 | component2 = "uuid2" 158 | changeentry1 = testhelper.createchangeentry(revision="anyRevId", component=component1) 159 | changeentry2 = testhelper.createchangeentry(revision="component2RevId", component=component2) 160 | changeentry3 = testhelper.createchangeentry(revision="anyOtherRevId", component=component1) 161 | changeentry3.setAccepted() 162 | changeentries = [changeentry1, changeentry2, changeentry3] 163 | 164 | self.configBuilder.setrepourl("anyurl").setuseautomaticconflictresolution("True").setmaxchangesetstoaccepttogether(10).setworkspace("anyWs") 165 | config = self.configBuilder.build() 166 | configuration.config = config 167 | 168 | handler = ImportHandler() 169 | handler.retryacceptincludingnextchangesets(changeentry1, changeentries) 170 | inputmock.assert_called_once_with('Do you want to continue? Y for continue, any key for abort') 171 | exitmock.assert_called_once_with("Please check the output/log and rerun program with resume") 172 | 173 | def test_collectChangeSetsToAcceptToAvoidMergeConflict_ShouldCollectThreeChangesets(self): 174 | change1 = testhelper.createchangeentry("1") 175 | change2 = testhelper.createchangeentry("2") 176 | change3 = testhelper.createchangeentry("3") 177 | 178 | changeentries = [change1, change2, change3] 179 | 180 | configuration.config = self.configBuilder.build() 181 | collectedchanges = ImportHandler().collect_changes_to_accept_to_avoid_conflicts(change1, changeentries, 10) 182 | self.assertTrue(change1 in collectedchanges) 183 | self.assertTrue(change2 in collectedchanges) 184 | self.assertTrue(change3 in collectedchanges) 185 | self.assertEqual(3, len(collectedchanges)) 186 | 187 | def test_collectChangeSetsToAcceptToAvoidMergeConflict_ShouldCollectOnlyUnacceptedChangesets(self): 188 | change1 = testhelper.createchangeentry(revision="1") 189 | change2 = testhelper.createchangeentry(revision="2") 190 | change2.setAccepted() 191 | change3 = testhelper.createchangeentry(revision="3") 192 | 193 | changeentries = [change1, change2, change3] 194 | 195 | configuration.config = self.configBuilder.build() 196 | collectedchanges = ImportHandler().collect_changes_to_accept_to_avoid_conflicts(change1, changeentries, 10) 197 | self.assertTrue(change1 in collectedchanges) 198 | self.assertFalse(change2 in collectedchanges) 199 | self.assertTrue(change3 in collectedchanges) 200 | self.assertEqual(2, len(collectedchanges)) 201 | 202 | def test_collectChangeSetsToAcceptToAvoidMergeConflict_ShouldAdhereToMaxChangeSetCount(self): 203 | change1 = testhelper.createchangeentry("1") 204 | change2 = testhelper.createchangeentry("2") 205 | change3 = testhelper.createchangeentry("3") 206 | 207 | changeentries = [change1, change2, change3] 208 | 209 | configuration.config = self.configBuilder.build() 210 | collectedchanges = ImportHandler().collect_changes_to_accept_to_avoid_conflicts(change1, changeentries, 2) 211 | self.assertTrue(change1 in collectedchanges) 212 | self.assertTrue(change2 in collectedchanges) 213 | self.assertFalse(change3 in collectedchanges) 214 | self.assertEqual(2, len(collectedchanges)) 215 | 216 | def test_collectChangeSetsToAcceptToAvoidMergeConflict_ShouldAcceptLargeAmountOfChangeSets(self): 217 | changeentries = [testhelper.createchangeentry(str(i)) for i in range(1, 500)] 218 | change1 = changeentries[0] 219 | 220 | configuration.config = self.configBuilder.build() 221 | collectedchanges = ImportHandler().collect_changes_to_accept_to_avoid_conflicts(change1, changeentries, 500) 222 | self.assertEqual(499, len(collectedchanges)) 223 | 224 | @patch('builtins.input', return_value='Y') 225 | def test_useragreeing_answeris_y_expecttrue(self, inputmock): 226 | configuration.config = self.configBuilder.build() 227 | self.assertTrue(ImportHandler().is_user_agreeing_to_accept_next_change(testhelper.createchangeentry())) 228 | 229 | @patch('builtins.input', return_value='n') 230 | def test_useragreeing_answeris_n_expectfalseandexception(self, inputmock): 231 | configuration.config = self.configBuilder.build() 232 | try: 233 | ImportHandler().is_user_agreeing_to_accept_next_change(testhelper.createchangeentry()) 234 | self.fail("Should have exit the program") 235 | except SystemExit as e: 236 | self.assertEqual("Please check the output/log and rerun program with resume", e.code) 237 | 238 | @patch('rtcFunctions.shell') 239 | @patch('rtcFunctions.Commiter') 240 | def test_load(self, commitermock, shellmock): 241 | anyurl = "anyUrl" 242 | config = self.configBuilder.setrepourl(anyurl).setworkspace(self.workspace).build() 243 | configuration.config = config 244 | WorkspaceHandler().load() 245 | expected_load_command = "lscm load -r %s %s --force" % (anyurl, self.workspace) 246 | shellmock.execute.assert_called_once_with(expected_load_command) 247 | calls = [call.get_untracked_statuszlines(), call.restore_shed_gitignore(commitermock.get_untracked_statuszlines())] 248 | commitermock.assert_has_calls(calls) 249 | 250 | @patch('rtcFunctions.shell') 251 | @patch('rtcFunctions.Commiter') 252 | def test_load_includecomponentroots(self, commitermock, shellmock): 253 | anyurl = "anyUrl" 254 | config = self.configBuilder.setrepourl(anyurl).setworkspace(self.workspace).setincludecomponentroots("True").build() 255 | configuration.config = config 256 | WorkspaceHandler().load() 257 | expected_load_command = "lscm load -r %s %s --force --include-root" % (anyurl, self.workspace) 258 | shellmock.execute.assert_called_once_with(expected_load_command) 259 | shellmock.execute.assert_called_once_with(expected_load_command) 260 | calls = [call.get_untracked_statuszlines(), call.restore_shed_gitignore(commitermock.get_untracked_statuszlines())] 261 | commitermock.assert_has_calls(calls) 262 | 263 | def test_CreateChangeEntry_minimal(self): 264 | revision="anyRevisionId" 265 | author="anyAuthor" 266 | email="anyEmail" 267 | date="anyDate" 268 | comment="anyComment" 269 | change = ChangeEntry(revision, author, email, date, comment) 270 | self.assertEqual(revision, change.revision) 271 | self.assertEqual(author, change.author) 272 | self.assertEqual(email, change.email) 273 | self.assertEqual(date, change.date) 274 | self.assertEqual(comment, change.comment) 275 | self.assertEqual("unknown", change.component) 276 | self.assertEqual(False, change.accepted) 277 | 278 | def test_CreateChangeEntry_component(self): 279 | revision="anyRevisionId" 280 | author="anyAuthor" 281 | email="anyEmail" 282 | date="anyDate" 283 | comment="anyComment" 284 | component = "anyComponent" 285 | change = ChangeEntry(revision, author, email, date, comment, component) 286 | self.assertEqual(revision, change.revision) 287 | self.assertEqual(author, change.author) 288 | self.assertEqual(email, change.email) 289 | self.assertEqual(date, change.date) 290 | self.assertEqual(comment, change.comment) 291 | self.assertEqual(component, change.component) 292 | self.assertEqual(False, change.accepted) 293 | 294 | def test_ChangeEntry_flip_accepted(self): 295 | change = testhelper.createchangeentry() 296 | self.assertEqual(False, change.accepted) 297 | change.setAccepted() 298 | self.assertEqual(True, change.accepted) 299 | change.setUnaccepted() 300 | self.assertEqual(False, change.accepted) 301 | 302 | def test_ChangeEntry_tostring(self): 303 | change = testhelper.createchangeentry(revision="anyRev", component="anyCmp") 304 | self.assertEqual("anyComment (Date: 2015-01-22, Author: anyAuthor, Revision: anyRev, Component: anyCmp, Accepted: False)", 305 | change.tostring()) 306 | 307 | @patch('rtcFunctions.shell') 308 | def test_getchangeentriesbytypeandvalue_type_stream(self, shellmock): 309 | anyurl = "anyUrl" 310 | config = self.configBuilder.setrepourl(anyurl).setworkspace(self.workspace).build() 311 | configuration.config = config 312 | stream = "myStreamUUID" 313 | comparetype = CompareType.stream 314 | comparetypename = comparetype.name 315 | filename = "Compare_%s_%s.txt" % (comparetypename, stream) 316 | outputfilename = config.getlogpath(filename) 317 | try: 318 | shellmock.encoding = 'UTF-8' 319 | ImportHandler().getchangeentriesbytypeandvalue(comparetype, stream) 320 | except FileNotFoundError: 321 | pass # do not bother creating the output file here 322 | expected_compare_command = "lscm --show-alias n --show-uuid y compare ws %s %s %s -r %s -I swc -C @@{name}@@{email}@@ --flow-directions i -D @@\"yyyy-MM-dd HH:mm:ss\"@@" \ 323 | % (self.workspace, comparetypename, stream, anyurl) 324 | shellmock.execute.assert_called_once_with(expected_compare_command, outputfilename) 325 | 326 | @patch('rtcFunctions.shell') 327 | def test_getchangeentriesbytypeandvalue_type_baseline(self, shellmock): 328 | anyurl = "anyUrl" 329 | config = self.configBuilder.setrepourl(anyurl).setworkspace(self.workspace).build() 330 | configuration.config = config 331 | baseline = "myBaselineUUID" 332 | comparetype = CompareType.baseline 333 | comparetypename = comparetype.name 334 | filename = "Compare_%s_%s.txt" % (comparetypename, baseline) 335 | outputfilename = config.getlogpath(filename) 336 | try: 337 | shellmock.encoding = 'UTF-8' 338 | ImportHandler().getchangeentriesbytypeandvalue(comparetype, baseline) 339 | except FileNotFoundError: 340 | pass # do not bother creating the output file here 341 | expected_compare_command = "lscm --show-alias n --show-uuid y compare ws %s %s %s -r %s -I swc -C @@{name}@@{email}@@ --flow-directions i -D @@\"yyyy-MM-dd HH:mm:ss\"@@" \ 342 | % (self.workspace, comparetypename, baseline, anyurl) 343 | shellmock.execute.assert_called_once_with(expected_compare_command, outputfilename) 344 | 345 | def test_getnextchangeset_fromsamecomponent_expectsamecomponent(self): 346 | component1 = "uuid_1" 347 | component2 = "uuid_2" 348 | # entries for component 1 (2nd entry being already accepted) 349 | entry1_1 = testhelper.createchangeentry(revision="1.1", component=component1) 350 | entry1_2 = testhelper.createchangeentry(revision="1.2", component=component1) 351 | entry1_2.setAccepted() 352 | entry1_3 = testhelper.createchangeentry(revision="1.3", component=component1) 353 | # entries for component 2 (2nd entry being already accepted) 354 | entry2_1 = testhelper.createchangeentry(revision="2.1", component=component2) 355 | entry2_2 = testhelper.createchangeentry(revision="2.2", component=component2) 356 | entry2_2.setAccepted() 357 | entry2_3 = testhelper.createchangeentry(revision="2.3", component=component2) 358 | changeentries = [] 359 | changeentries.append(entry1_1) 360 | changeentries.append(entry2_1) 361 | changeentries.append(entry1_2) 362 | changeentries.append(entry2_2) 363 | changeentries.append(entry1_3) 364 | changeentries.append(entry2_3) 365 | 366 | nextentry = ImportHandler.getnextchangeset_fromsamecomponent(currentchangeentry=entry2_1, changeentries=changeentries) 367 | self.assertIsNotNone(nextentry) 368 | self.assertFalse(nextentry.isAccepted()) 369 | self.assertEqual(component2, nextentry.component) 370 | self.assertEqual("2.3", nextentry.revision) 371 | 372 | def test_getnextchangeset_fromsamecomponent_expectnonefound(self): 373 | component1 = "uuid_1" 374 | component2 = "uuid_2" 375 | # entries for component 1 376 | entry1_1 = testhelper.createchangeentry(revision="1.1", component=component1) 377 | entry1_1.setAccepted() 378 | entry1_2 = testhelper.createchangeentry(revision="1.2", component=component1) 379 | entry1_2.setAccepted() 380 | # entries for component 2 (2nd entry being already accepted) 381 | entry2_1 = testhelper.createchangeentry(revision="2.1", component=component2) 382 | entry2_1.setAccepted() 383 | entry2_2 = testhelper.createchangeentry(revision="2.2", component=component2) 384 | entry2_2.setAccepted() 385 | changeentries = [] 386 | changeentries.append(entry1_1) 387 | changeentries.append(entry2_1) 388 | changeentries.append(entry1_2) 389 | changeentries.append(entry2_2) 390 | 391 | nextentry = ImportHandler.getnextchangeset_fromsamecomponent(currentchangeentry=entry2_1, changeentries=changeentries) 392 | self.assertIsNone(nextentry) 393 | 394 | def get_Sample_File_Path(self, filename): 395 | testpath = os.path.realpath(__file__) 396 | testdirectory = os.path.dirname(testpath) 397 | testfilename = os.path.splitext(os.path.basename(testpath))[0] 398 | sample_file_path = testdirectory + os.sep + "resources" + os.sep + testfilename + "_" + filename 399 | return sample_file_path 400 | 401 | def assert_Change_Entry(self, change, author, email, comment, date, component, accepted=False): 402 | self.assertIsNotNone(change) 403 | self.assertEqual(author, change.author) 404 | self.assertEqual(email, change.email) 405 | self.assertEqual(comment, change.comment) 406 | self.assertEqual(date, change.date) 407 | self.assertEqual(component, change.component) 408 | self.assertEqual(accepted, change.accepted) 409 | -------------------------------------------------------------------------------- /tests/test_shell.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import patch 3 | 4 | import shell 5 | 6 | 7 | class ShellTest(unittest.TestCase): 8 | def setUp(self): 9 | shell.encoding = None 10 | 11 | @patch('shell.shouter') 12 | @patch('shell.call') 13 | def testWhenLoggingShellComandsIsDisabled_ExpectNoOutput(self, subprocess_call_mock, shouter_mock): 14 | shell.logcommands = False 15 | shell.execute("doSomething") 16 | assert not shouter_mock.shout.called 17 | 18 | @patch('shell.shouter') 19 | @patch('shell.call') 20 | def testWhenLoggingShellComandsIsEnabled_ExpectCommandIsLoggedToOutput(self, subprocess_call_mock, shouter_mock): 21 | shell.logcommands = True 22 | shell.execute("doSomething") 23 | assert shouter_mock.shout.called 24 | 25 | def testSetEncodingUTF8_ShouldBeUTF8(self): 26 | encoding = "UTF-8" 27 | shell.setencoding(encoding) 28 | self.assertEqual(encoding, shell.encoding) 29 | 30 | def testSetNoEncoding_ShouldBeNone(self): 31 | shell.setencoding("") 32 | self.assertIsNone(shell.encoding) 33 | 34 | def testEscapeShellVariableExpansion(self): 35 | self.assertEqual('my simple comment', shell.escapeShellVariableExpansion('my simple comment')) 36 | self.assertEqual('my simple "\$"variable comment', shell.escapeShellVariableExpansion('my simple $variable comment')) 37 | -------------------------------------------------------------------------------- /tests/test_sorter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from rtcFunctions import ChangeEntry 4 | import sorter 5 | 6 | 7 | class SorterTest(unittest.TestCase): 8 | def setUp(self): 9 | self.changeentriesmap = {} 10 | self.changesetcounter = 0 11 | 12 | def testSortingTwoComponents_ExpectKeepOrderingInsideSameKey(self): 13 | firstkey = "anyKey" 14 | secondkey = "anyOtherKey" 15 | self.put(firstkey, self.createchangeentry("1991-03-24")) 16 | self.put(firstkey, self.createchangeentry("1991-05-24")) 17 | self.put(firstkey, self.createchangeentry("1991-05-22")) 18 | self.put(secondkey, self.createchangeentry("1991-05-23")) 19 | self.put(secondkey, self.createchangeentry("1991-07-22")) 20 | sortedchangesets = sorter.tosortedlist(self.changeentriesmap) 21 | 22 | self.assertEqual("1991-03-24", sortedchangesets[0].date) # earliestEntry firstkey 23 | self.assertEqual("1991-05-23", sortedchangesets[1].date) # earliestEntry secondKey 24 | self.assertEqual("1991-05-24", sortedchangesets[2].date) # secondEarliestEntry firstKey 25 | self.assertEqual("1991-05-22", sortedchangesets[3].date) # lastEntry firstKey 26 | self.assertEqual("1991-07-22", sortedchangesets[4].date) # lastEntry secondKey 27 | 28 | def testSortingThreeComponents_ExpectDatesInOrder(self): 29 | earliestdate = "2015-01-30 09:00:15" 30 | dateinbetween = "2015-01-30 09:00:16" 31 | latestdate = "2015-02-30 09:00:00" 32 | self.put("justAnotherKey", self.createchangeentry(latestdate)) 33 | self.put("anyKey", self.createchangeentry(earliestdate)) 34 | self.put("anyOtherKey", self.createchangeentry(dateinbetween)) 35 | 36 | sortedchangesets = sorter.tosortedlist(self.changeentriesmap) 37 | self.assertEqual(earliestdate, sortedchangesets[0].date) 38 | self.assertEqual(dateinbetween, sortedchangesets[1].date) 39 | self.assertEqual(latestdate, sortedchangesets[2].date) 40 | 41 | def createchangeentry(self, date): 42 | self.changesetcounter += 1 43 | return ChangeEntry(self.changesetcounter, "anyAuthor", "anyEmail", date, "") 44 | 45 | def put(self, key, changeentry): 46 | changeentries = self.changeentriesmap.get(key) 47 | if not changeentries: 48 | changeentries = [] 49 | changeentries.append(changeentry) 50 | self.changeentriesmap[key] = changeentries 51 | -------------------------------------------------------------------------------- /tests/testhelper.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import tempfile 3 | import os 4 | import shutil 5 | 6 | from configuration import Builder 7 | from gitFunctions import Initializer 8 | from rtcFunctions import ChangeEntry 9 | import configuration 10 | 11 | 12 | @contextmanager 13 | def mkchdir(subfolder, folderprefix="rtc2test_case"): 14 | tempfolder = tempfile.mkdtemp(prefix=folderprefix + subfolder) 15 | previousdir = os.getcwd() 16 | os.chdir(tempfolder) 17 | try: 18 | yield tempfolder 19 | finally: 20 | os.chdir(previousdir) 21 | shutil.rmtree(tempfolder, ignore_errors=True) # on windows folder remains in temp, git process locks it 22 | 23 | 24 | @contextmanager 25 | def createrepo(reponame="test.git", folderprefix="rtc2test_case"): 26 | repodir = tempfile.mkdtemp(prefix=folderprefix) 27 | configuration.config = Builder().setworkdirectory(repodir).setgitreponame(reponame).build() 28 | initializer = Initializer() 29 | previousdir = os.getcwd() 30 | os.chdir(repodir) 31 | initializer.initalize() 32 | try: 33 | yield 34 | finally: 35 | os.chdir(previousdir) 36 | shutil.rmtree(repodir, ignore_errors=True) # on windows folder remains in temp, git process locks it 37 | 38 | 39 | @contextmanager 40 | def cd(newdir): 41 | """ 42 | Change directory to newdir and return to the previous upon completion 43 | :param newdir: directory to change to 44 | """ 45 | previousdir = os.getcwd() 46 | os.chdir(os.path.expanduser(newdir)) 47 | try: 48 | yield 49 | finally: 50 | os.chdir(previousdir) 51 | 52 | 53 | def getrelativefilename(filenamerelativetotests): 54 | """ 55 | Determine the correct relative file name, depending on the test runtime environment. 56 | Default environment ist PyCharm, which sets the working directory to /test. 57 | However, if the tests are run with 'python3 -m unittest discover -s tests', the working directory is one level above. 58 | 59 | :param filenamerelativetotests: 60 | :return:the correct relative file name 61 | """ 62 | directory = os.getcwd() 63 | if directory.endswith(os.sep + "tests"): 64 | relativefilename = filenamerelativetotests 65 | else: 66 | if filenamerelativetotests.startswith(".." + os.sep): 67 | relativefilename = filenamerelativetotests[1:] 68 | elif filenamerelativetotests.startswith("." + os.sep): 69 | relativefilename = 'tests' + os.sep + filenamerelativetotests[2:] 70 | else: 71 | relativefilename = 'tests' + os.sep + filenamerelativetotests 72 | return relativefilename 73 | 74 | 75 | def createchangeentry(revision="anyRevisionId", author="anyAuthor", email="anyEmail", comment="anyComment", 76 | date="2015-01-22", component="anyComponentUUID"): 77 | return ChangeEntry(revision, author, email, date, comment, component) 78 | --------------------------------------------------------------------------------