diff --git a/build.gradle b/build.gradle index 33b653a5..d104a6f3 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ dependencies { compile 'org.eclipse.jgit:org.eclipse.jgit:3.6.2.201501210735-r' compile 'com.github.spullara.mustache.java:compiler:0.8.18' compile 'org.gitlab:java-gitlab-api:4.1.0' + compile 'org.apache.commons:commons-lang3:3.9' compileOnly 'com.github.spotbugs:spotbugs-annotations:3.1.12' diff --git a/src/main/java/se/bjurr/gitchangelog/api/GitChangelogApi.java b/src/main/java/se/bjurr/gitchangelog/api/GitChangelogApi.java index 36c2df5d..209d430d 100644 --- a/src/main/java/se/bjurr/gitchangelog/api/GitChangelogApi.java +++ b/src/main/java/se/bjurr/gitchangelog/api/GitChangelogApi.java @@ -27,9 +27,7 @@ import java.io.Writer; import java.net.URL; import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; -import java.util.Map; +import java.util.*; import org.eclipse.jgit.lib.ObjectId; import se.bjurr.gitchangelog.api.exceptions.GitChangelogIntegrationException; import se.bjurr.gitchangelog.api.exceptions.GitChangelogRepositoryException; @@ -37,6 +35,7 @@ import se.bjurr.gitchangelog.api.model.Issue; import se.bjurr.gitchangelog.internal.git.GitRepo; import se.bjurr.gitchangelog.internal.git.GitRepoData; +import se.bjurr.gitchangelog.internal.git.GitSubmoduleParser; import se.bjurr.gitchangelog.internal.git.model.GitCommit; import se.bjurr.gitchangelog.internal.git.model.GitTag; import se.bjurr.gitchangelog.internal.integrations.mediawiki.MediaWikiClient; @@ -380,6 +379,12 @@ public GitChangelogApi withSettings(final URL url) { return this; } + /** {@link Settings}. */ + public GitChangelogApi withSettings(final Settings settings) { + this.settings = settings; + return this; + } + /** Use string as template. {@link #withTemplatePath}. */ public GitChangelogApi withTemplateContent(final String templateContent) { this.templateContent = templateContent; @@ -465,6 +470,14 @@ private Changelog getChangelog(final GitRepo gitRepo, final boolean useIntegrati diff = gitRepoData.getGitCommits(); } final List tags = gitRepoData.getGitTags(); + + List submodules = new ArrayList<>(); + if (gitRepo.hasSubmodules()) { + submodules = + new GitSubmoduleParser() + .parseForSubmodules(this, useIntegrationIfConfigured, gitRepo, diff); + } + final Transformer transformer = new Transformer(this.settings); return new Changelog( // transformer.toCommits(diff), // @@ -472,6 +485,7 @@ private Changelog getChangelog(final GitRepo gitRepo, final boolean useIntegrati transformer.toAuthors(diff), // transformer.toIssues(issues), // transformer.toIssueTypes(issues), // + submodules, // gitRepoData.findOwnerName().orNull(), // gitRepoData.findRepoName().orNull()); } diff --git a/src/main/java/se/bjurr/gitchangelog/api/model/Changelog.java b/src/main/java/se/bjurr/gitchangelog/api/model/Changelog.java index e82f801b..7ef260a6 100644 --- a/src/main/java/se/bjurr/gitchangelog/api/model/Changelog.java +++ b/src/main/java/se/bjurr/gitchangelog/api/model/Changelog.java @@ -9,12 +9,13 @@ import se.bjurr.gitchangelog.api.model.interfaces.IIssues; public class Changelog implements ICommits, IAuthors, IIssues, Serializable { - private static final long serialVersionUID = 2193789018496738737L; + private static final long serialVersionUID = 2193789018496738738L; private final List commits; private final List tags; private final List authors; private final List issues; private final List issueTypes; + private final List submodules; private final String ownerName; private final String repoName; @@ -24,6 +25,7 @@ public Changelog( List authors, List issues, List issueTypes, + List submodules, String ownerName, String repoName) { this.commits = checkNotNull(commits, "commits"); @@ -31,6 +33,7 @@ public Changelog( this.authors = checkNotNull(authors, "authors"); this.issues = checkNotNull(issues, "issues"); this.issueTypes = checkNotNull(issueTypes, "issueTypes"); + this.submodules = checkNotNull(submodules, "submodules"); this.ownerName = ownerName; this.repoName = repoName; } @@ -65,4 +68,8 @@ public List getTags() { public List getIssueTypes() { return issueTypes; } + + public List getSubmodules() { + return submodules; + } } diff --git a/src/main/java/se/bjurr/gitchangelog/internal/git/GitRepo.java b/src/main/java/se/bjurr/gitchangelog/internal/git/GitRepo.java index 5f528d2f..a63554de 100644 --- a/src/main/java/se/bjurr/gitchangelog/internal/git/GitRepo.java +++ b/src/main/java/se/bjurr/gitchangelog/internal/git/GitRepo.java @@ -14,26 +14,20 @@ import com.google.common.base.Optional; import com.google.common.collect.Ordering; -import java.io.Closeable; -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.Comparator; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; +import java.io.*; +import java.util.*; import org.eclipse.jgit.api.Git; -import org.eclipse.jgit.lib.AnyObjectId; -import org.eclipse.jgit.lib.ObjectId; -import org.eclipse.jgit.lib.Ref; -import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.diff.DiffFormatter; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.*; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevTag; +import org.eclipse.jgit.revwalk.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.submodule.SubmoduleWalk; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.bjurr.gitchangelog.api.GitChangelogApiConstants; @@ -47,10 +41,12 @@ public class GitRepo implements Closeable { private Git git; private final Repository repository; private final RevWalk revWalk; + private final HashMap submodules; public GitRepo() { this.repository = null; this.revWalk = null; + this.submodules = new HashMap<>(); } public GitRepo(final File repo) throws GitChangelogRepositoryException { @@ -71,6 +67,23 @@ public GitRepo(final File repo) throws GitChangelogRepositoryException { this.repository = builder.build(); this.revWalk = new RevWalk(this.repository); this.git = new Git(this.repository); + + this.submodules = new HashMap<>(); + if (SubmoduleWalk.containsGitModulesFile(repository)) { + final SubmoduleWalk submoduleWalk = SubmoduleWalk.forIndex(repository); + while (submoduleWalk.next()) { + final Repository submoduleRepository = submoduleWalk.getRepository(); + if (submoduleRepository != null) { + try { + String submodulePath = submoduleWalk.getModulesPath(); + this.submodules.put(submodulePath, new GitRepo(submoduleRepository.getDirectory())); + } catch (ConfigInvalidException e) { + LOG.warn("invalid submodule configuration; skipping submodule\n" + e); + } + submoduleRepository.close(); + } + } + } } catch (final IOException e) { throw new GitChangelogRepositoryException( "Could not use GIT repo in " + repo.getAbsolutePath(), e); @@ -89,6 +102,10 @@ public void close() throws IOException { LOG.error(e.getMessage(), e); } } + + for (Map.Entry submodule : submodules.entrySet()) { + submodule.getValue().close(); + } } public ObjectId getCommit(final String fromCommit) throws GitChangelogRepositoryException { @@ -142,6 +159,45 @@ public ObjectId getRef(final String fromRef) throws GitChangelogRepositoryExcept throw new GitChangelogRepositoryException(fromRef + " not found in:\n" + toString()); } + public boolean hasSubmodules() { + return submodules.size() > 0; + } + + public GitRepo getSubmodule(String submodulePath) { + return submodules.getOrDefault(submodulePath, null); + } + + public String getDiff(String commitHash) throws GitChangelogRepositoryException { + RevCommit commit; + RevCommit prevCommit; + + try { + commit = this.revWalk.parseCommit(getCommit(commitHash)); + prevCommit = commit.getParentCount() > 0 ? commit.getParent(0) : null; + } catch (GitChangelogRepositoryException | IOException e) { + throw new GitChangelogRepositoryException("", e); + } + + OutputStream outputStream = new ByteArrayOutputStream(); + DiffFormatter diffFormatter = new DiffFormatter(outputStream); + diffFormatter.setRepository(this.repository); + diffFormatter.setAbbreviationLength(10); + + try { + for (DiffEntry entry : diffFormatter.scan(prevCommit, commit)) { + diffFormatter.format(diffFormatter.toFileHeader(entry)); + } + } catch (IOException e) { + throw new GitChangelogRepositoryException("", e); + } + + return outputStream.toString(); + } + + public String getDirectory() { + return this.repository.getDirectory().getAbsolutePath(); + } + @Override public String toString() { final StringBuilder sb = new StringBuilder(); @@ -151,6 +207,21 @@ public String toString() { return "Repo: " + this.repository + "\n" + sb.toString(); } + private CanonicalTreeParser getTreeParser(RevCommit commit) + throws GitChangelogRepositoryException { + RevTree revTree = commit.getTree(); + if (revTree == null) { + return null; + } + ObjectId treeId = commit.getTree().getId(); + ObjectReader reader = this.repository.newObjectReader(); + try { + return new CanonicalTreeParser(null, reader, treeId); + } catch (IOException e) { + throw new GitChangelogRepositoryException("", e); + } + } + private boolean addCommitToCurrentTag( final Map> commitsPerTagName, final String currentTagName, @@ -291,6 +362,10 @@ private List gitTags( final RevCommit from = this.revWalk.lookupCommit(fromObjectId); final RevCommit to = this.revWalk.lookupCommit(toObjectId); + if (from == to) { + return newArrayList(); + } + this.commitsToInclude = getDiffingCommits(from, to); final List tagList = tagsBetweenFromAndTo(from, to); diff --git a/src/main/java/se/bjurr/gitchangelog/internal/git/GitSubmoduleParser.java b/src/main/java/se/bjurr/gitchangelog/internal/git/GitSubmoduleParser.java new file mode 100644 index 00000000..b9001850 --- /dev/null +++ b/src/main/java/se/bjurr/gitchangelog/internal/git/GitSubmoduleParser.java @@ -0,0 +1,100 @@ +package se.bjurr.gitchangelog.internal.git; + +import static org.slf4j.LoggerFactory.getLogger; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.apache.commons.lang3.SerializationUtils; +import org.slf4j.Logger; +import se.bjurr.gitchangelog.api.GitChangelogApi; +import se.bjurr.gitchangelog.api.exceptions.GitChangelogRepositoryException; +import se.bjurr.gitchangelog.api.model.Changelog; +import se.bjurr.gitchangelog.internal.git.model.GitCommit; +import se.bjurr.gitchangelog.internal.settings.Settings; + +public class GitSubmoduleParser { + private static final Logger LOG = getLogger(GitSubmoduleParser.class); + + public GitSubmoduleParser() {} + + public List parseForSubmodules( + final GitChangelogApi gitChangelogApi, + final boolean useIntegrationIfConfigured, + final GitRepo gitRepo, + final List commits) + throws GitChangelogRepositoryException { + + List submodules = new ArrayList<>(); + Map submoduleEntries = new TreeMap<>(); + Pattern submoduleNamePattern = + Pattern.compile( + "(?m)^\\+{3} b/([\\w/\\s-]+)($\\n@.+)?\\n-Subproject commit (\\w+)$\\n\\+Subproject commit (\\w+)$"); + + for (GitCommit commit : commits) { + String diff = gitRepo.getDiff(commit.getHash()); + Matcher submoduleMatch = submoduleNamePattern.matcher(diff); + while (submoduleMatch.find()) { + String submoduleName = submoduleMatch.group(1); + String previousSubmoduleHash = submoduleMatch.group(3); + String currentSubmoduleHash = submoduleMatch.group(4); + GitRepo submodule = gitRepo.getSubmodule(submoduleName); + + if (submodule == null) { + continue; + } + + SubmoduleEntry submoduleEntry = + new SubmoduleEntry( + submoduleName, previousSubmoduleHash, currentSubmoduleHash, submodule); + + if (!submoduleEntries.containsKey(submoduleName)) { + submoduleEntries.put(submoduleName, submoduleEntry); + } + + SubmoduleEntry existingEntry = submoduleEntries.getOrDefault(submoduleName, submoduleEntry); + existingEntry.previousSubmoduleHash = submoduleEntry.previousSubmoduleHash; + } + } + + Settings settings = gitChangelogApi.getSettings(); + for (Map.Entry submoduleEntry : submoduleEntries.entrySet()) { + Settings submoduleSettings = SerializationUtils.clone(settings); + + submoduleSettings.setFromCommit(submoduleEntry.getValue().previousSubmoduleHash); + submoduleSettings.setToCommit(submoduleEntry.getValue().currentSubmoduleHash); + submoduleSettings.setFromRef(null); + submoduleSettings.setToRef(null); + submoduleSettings.setFromRepo(submoduleEntry.getValue().gitRepo.getDirectory()); + + try { + submodules.add( + GitChangelogApi.gitChangelogApiBuilder() + .withSettings(submoduleSettings) + .getChangelog(useIntegrationIfConfigured)); + } catch (GitChangelogRepositoryException e) { + throw new GitChangelogRepositoryException("", e); + } + } + + return submodules; + } + + private class SubmoduleEntry { + public final String name; + public String previousSubmoduleHash; + public final String currentSubmoduleHash; + public final GitRepo gitRepo; + + public SubmoduleEntry( + final String name, + final String previousSubmoduleHash, + final String currentSubmoduleHash, + final GitRepo gitRepo) { + this.name = name; + this.previousSubmoduleHash = previousSubmoduleHash; + this.currentSubmoduleHash = currentSubmoduleHash; + this.gitRepo = gitRepo; + } + } +} diff --git a/src/main/java/se/bjurr/gitchangelog/internal/model/Transformer.java b/src/main/java/se/bjurr/gitchangelog/internal/model/Transformer.java index 8600cb5c..fae028af 100644 --- a/src/main/java/se/bjurr/gitchangelog/internal/model/Transformer.java +++ b/src/main/java/se/bjurr/gitchangelog/internal/model/Transformer.java @@ -16,17 +16,9 @@ import com.google.common.base.Predicate; import com.google.common.collect.Multimap; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; -import se.bjurr.gitchangelog.api.model.Author; -import se.bjurr.gitchangelog.api.model.Commit; -import se.bjurr.gitchangelog.api.model.Issue; -import se.bjurr.gitchangelog.api.model.IssueType; -import se.bjurr.gitchangelog.api.model.Tag; +import se.bjurr.gitchangelog.api.model.*; import se.bjurr.gitchangelog.internal.git.model.GitCommit; import se.bjurr.gitchangelog.internal.git.model.GitTag; import se.bjurr.gitchangelog.internal.settings.IssuesUtil; diff --git a/src/test/java/se/bjurr/gitchangelog/api/GitChangelogWithSubmodulesApiTest.java b/src/test/java/se/bjurr/gitchangelog/api/GitChangelogWithSubmodulesApiTest.java new file mode 100644 index 00000000..a8ef4cde --- /dev/null +++ b/src/test/java/se/bjurr/gitchangelog/api/GitChangelogWithSubmodulesApiTest.java @@ -0,0 +1,137 @@ +package se.bjurr.gitchangelog.api; + +import static com.google.common.base.Charsets.UTF_8; +import static com.google.common.io.Resources.getResource; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static se.bjurr.gitchangelog.api.GitChangelogApi.gitChangelogApiBuilder; +import static se.bjurr.gitchangelog.api.GitChangelogApiConstants.ZERO_COMMIT; + +import com.google.common.io.Resources; +import com.google.gson.GsonBuilder; +import java.io.*; +import java.net.URL; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.apache.commons.io.FileUtils; +import org.eclipse.jgit.api.Git; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class GitChangelogWithSubmodulesApiTest { + private static final String REPOSITORY_PATH = "submodule-test-repo"; + private File gitRepoFile; + + @Before + public void before() { + + File fileZip = + new File(Resources.getResource(String.format("%s.zip", REPOSITORY_PATH)).getFile()); + File destDir = fileZip.getParentFile(); + byte[] buffer = new byte[1024]; + try { + ZipInputStream zis = new ZipInputStream(new FileInputStream(fileZip)); + ZipEntry zipEntry = zis.getNextEntry(); + while (zipEntry != null) { + File newFile = new File(destDir, zipEntry.getName()); + if (zipEntry.isDirectory()) { + newFile.mkdirs(); + } else { + newFile.getParentFile().mkdirs(); + FileOutputStream fos = new FileOutputStream(newFile); + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + fos.close(); + } + zipEntry = zis.getNextEntry(); + } + zis.closeEntry(); + zis.close(); + } catch (FileNotFoundException e) { + fail("Could not find zipped repository"); + } catch (IOException e) { + fail("Could not unzip repository"); + } + this.gitRepoFile = new File(Resources.getResource(REPOSITORY_PATH).getFile()); + } + + @After + public void after() { + try { + FileUtils.deleteDirectory(new File(Resources.getResource(REPOSITORY_PATH).getFile())); + } catch (IOException e) { + fail("Could not cleanup repository folder"); + } + } + + @Test + public void testThatSubmoduleChangesAreListed() throws Exception { + final String expected = + Resources.toString(getResource("templatetest/testThatSubmoduleChangesAreListed.md"), UTF_8) + .trim(); + + final URL settingsFile = + getResource("settings/git-changelog-test-settings.json").toURI().toURL(); + final String templatePath = "templatetest/testSubmodules.mustache"; + + final String templateContent = Resources.toString(getResource(templatePath), UTF_8); + + final GitChangelogApi changelogApiBuilder = + gitChangelogApiBuilder() + .withSettings(settingsFile) + .withFromRepo(gitRepoFile.getAbsolutePath()) + .withFromCommit(ZERO_COMMIT) + .withToRef("master") + .withTemplatePath(templatePath); + + assertEquals( + "templateContent:\n" + + templateContent + + "\nContext:\n" + + toJson(changelogApiBuilder.getChangelog(true)), + expected, + changelogApiBuilder.render().trim()); + } + + @Test + public void testThatSubmoduleChangesAreNotListedWhenSubmoduleIsRemoved() throws Exception { + final String expected = + Resources.toString( + getResource( + "templatetest/testThatSubmoduleChangesAreNotListedWhenSubmoduleIsRemoved.md"), + UTF_8) + .trim(); + + final URL settingsFile = + getResource("settings/git-changelog-test-settings.json").toURI().toURL(); + final String templatePath = "templatetest/testSubmodules.mustache"; + + final String templateContent = Resources.toString(getResource(templatePath), UTF_8); + + Git git = Git.open(gitRepoFile); + git.checkout().setName("RemovedSubmodule").call(); + + final GitChangelogApi changelogApiBuilder = + gitChangelogApiBuilder() + .withSettings(settingsFile) + .withFromRepo(gitRepoFile.getAbsolutePath()) + .withFromCommit(ZERO_COMMIT) + .withToRef("RemovedSubmodule") + .withTemplatePath(templatePath); + + assertEquals( + "templateContent:\n" + + templateContent + + "\nContext:\n" + + toJson(changelogApiBuilder.getChangelog(true)), + expected, + changelogApiBuilder.render().trim()); + } + + private String toJson(final Object object) { + return new GsonBuilder().setPrettyPrinting().create().toJson(object); + } +} diff --git a/src/test/resources/submodule-test-repo.zip b/src/test/resources/submodule-test-repo.zip new file mode 100644 index 00000000..3d298fbe Binary files /dev/null and b/src/test/resources/submodule-test-repo.zip differ diff --git a/src/test/resources/templatetest/testSubmodules.mustache b/src/test/resources/templatetest/testSubmodules.mustache new file mode 100644 index 00000000..2b2abef1 --- /dev/null +++ b/src/test/resources/templatetest/testSubmodules.mustache @@ -0,0 +1,44 @@ +# Git Changelog changelog + +Changelog of Git Changelog. +{{#tags}} + {{name}} + {{#issues}} + {{#hasLink}} + ## {{name}} [{{issue}}]({{link}}) {{title}} + {{/hasLink}} + {{^hasLink}} + ## {{name}} {{title}} + {{/hasLink}} + + {{#commits}} + ### {{authorName}} - {{commitTime}} + [{{hash}}](https://server/{{hash}}) + + {{{message}}} + + {{/commits}} + {{/issues}} +{{/tags}} +{{#submodules}} + {{repoName}} + {{#tags}} + {{name}} + {{#issues}} + {{#hasLink}} + ## {{name}} [{{issue}}]({{link}}) {{title}} + {{/hasLink}} + {{^hasLink}} + ## {{name}} {{title}} + {{/hasLink}} + + {{#commits}} + ### {{authorName}} - {{commitTime}} + [{{hash}}](https://server/{{hash}}) + + {{{message}}} + + {{/commits}} + {{/issues}} + {{/tags}} +{{/submodules}} \ No newline at end of file diff --git a/src/test/resources/templatetest/testThatSubmoduleChangesAreListed.md b/src/test/resources/templatetest/testThatSubmoduleChangesAreListed.md new file mode 100644 index 00000000..4204f769 --- /dev/null +++ b/src/test/resources/templatetest/testThatSubmoduleChangesAreListed.md @@ -0,0 +1,63 @@ +# Git Changelog changelog + +Changelog of Git Changelog. + No tag + ## No issue supplied + + ### nemui - 2020-09-09 10:02:16 + [ee3f3ad730f5727](https://server/ee3f3ad730f5727) + + update submodule hashes + + ### nemui - 2020-09-09 10:00:18 + [548afb9a6807ef7](https://server/548afb9a6807ef7) + + add submodule library-b + + ### nemui - 2020-09-09 09:57:19 + [36bc99ea0082346](https://server/36bc99ea0082346) + + changes both in the repository and in the submodule + + ### nemui - 2020-09-09 09:54:03 + [dceeed792cd595a](https://server/dceeed792cd595a) + + update submodule hashes + + ### nemui - 2020-09-09 09:49:39 + [ffabb2c14e905d8](https://server/ffabb2c14e905d8) + + add library-a + + ### nemui - 2020-09-09 09:48:43 + [96df4818b3f4fbd](https://server/96df4818b3f4fbd) + + update README + + library-a + No tag + ## No issue supplied + + ### Dmitry Mamchur - 2020-09-09 10:01:38 + [d348aa363c0e0a1](https://server/d348aa363c0e0a1) + + remove newline characters + + ### Dmitry Mamchur - 2020-09-09 09:56:19 + [ed44531560d0529](https://server/ed44531560d0529) + + add new lines + + ### Dmitry Mamchur - 2020-09-09 09:53:33 + [1320038ff746b37](https://server/1320038ff746b37) + + missing punctuation mark + + library-b + No tag + ## No issue supplied + + ### Dmitry Mamchur - 2020-09-09 10:01:57 + [bceaf8876328d47](https://server/bceaf8876328d47) + + add newline characters \ No newline at end of file diff --git a/src/test/resources/templatetest/testThatSubmoduleChangesAreNotListedWhenSubmoduleIsRemoved.md b/src/test/resources/templatetest/testThatSubmoduleChangesAreNotListedWhenSubmoduleIsRemoved.md new file mode 100644 index 00000000..e63cca7e --- /dev/null +++ b/src/test/resources/templatetest/testThatSubmoduleChangesAreNotListedWhenSubmoduleIsRemoved.md @@ -0,0 +1,49 @@ +# Git Changelog changelog + +Changelog of Git Changelog. + No tag + ## No issue supplied + + ### nemui - 2020-09-09 10:12:32 + [1f1fc961afa5926](https://server/1f1fc961afa5926) + + remove library-a submodule + + ### nemui - 2020-09-09 10:02:16 + [ee3f3ad730f5727](https://server/ee3f3ad730f5727) + + update submodule hashes + + ### nemui - 2020-09-09 10:00:18 + [548afb9a6807ef7](https://server/548afb9a6807ef7) + + add submodule library-b + + ### nemui - 2020-09-09 09:57:19 + [36bc99ea0082346](https://server/36bc99ea0082346) + + changes both in the repository and in the submodule + + ### nemui - 2020-09-09 09:54:03 + [dceeed792cd595a](https://server/dceeed792cd595a) + + update submodule hashes + + ### nemui - 2020-09-09 09:49:39 + [ffabb2c14e905d8](https://server/ffabb2c14e905d8) + + add library-a + + ### nemui - 2020-09-09 09:48:43 + [96df4818b3f4fbd](https://server/96df4818b3f4fbd) + + update README + + library-b + No tag + ## No issue supplied + + ### Dmitry Mamchur - 2020-09-09 10:01:57 + [bceaf8876328d47](https://server/bceaf8876328d47) + + add newline characters \ No newline at end of file