Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package git-filter-repo for openSUSE:Factory
checked in at 2022-12-05 18:01:06
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/git-filter-repo (Old)
and /work/SRC/openSUSE:Factory/.git-filter-repo.new.1835 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "git-filter-repo"
Mon Dec 5 18:01:06 2022 rev:7 rq:1040055 version:2.38.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/git-filter-repo/git-filter-repo.changes
2021-12-18 20:30:51.722263069 +0100
+++
/work/SRC/openSUSE:Factory/.git-filter-repo.new.1835/git-filter-repo.changes
2022-12-05 18:01:10.840574891 +0100
@@ -1,0 +2,6 @@
+Sun Dec 4 15:52:06 UTC 2022 - Dirk Müller <[email protected]>
+
+- update to 2.38.0:
+ https://github.com/newren/git-filter-repo/compare/v2.34.0...v2.38.0
+
+-------------------------------------------------------------------
Old:
----
git-filter-repo-2.34.0.tar.xz
New:
----
git-filter-repo-2.38.0.tar.xz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ git-filter-repo.spec ++++++
--- /var/tmp/diff_new_pack.AB6q6J/_old 2022-12-05 18:01:11.500578485 +0100
+++ /var/tmp/diff_new_pack.AB6q6J/_new 2022-12-05 18:01:11.512578550 +0100
@@ -1,7 +1,7 @@
#
# spec file for package git-filter-repo
#
-# Copyright (c) 2021 SUSE LLC
+# Copyright (c) 2022 SUSE LLC
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -22,21 +22,17 @@
%global gitexecdir %{_libexecdir}/git
Name: git-filter-repo
-Version: 2.34.0
+Version: 2.38.0
Release: 0
Summary: Quickly rewrite git repository history (git-filter-branch
replacement)
License: GPL-2.0-only OR MIT
Group: Development/Tools/Version Control
URL: https://github.com/newren/git-filter-repo
-#
Source0:
https://github.com/newren/git-filter-repo/releases/download/v%{version}/%{name}-%{version}.tar.xz
-#
BuildArch: noarch
-#
BuildRequires: %{python_module devel}
-BuildRequires: git
-#
-Requires: git
+BuildRequires: git-core
+Requires: git-core
%description
git filter-repo is a versatile tool for rewriting history, which includes
++++++ git-filter-repo-2.34.0.tar.xz -> git-filter-repo-2.38.0.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/git-filter-repo-2.34.0/Documentation/git-filter-repo.txt
new/git-filter-repo-2.38.0/Documentation/git-filter-repo.txt
--- old/git-filter-repo-2.34.0/Documentation/git-filter-repo.txt
2021-11-15 23:57:06.000000000 +0100
+++ new/git-filter-repo-2.38.0/Documentation/git-filter-repo.txt
2022-10-10 20:10:19.000000000 +0200
@@ -359,7 +359,7 @@
------
Every time filter-repo is run, files are created in the `.git/filter-repo/`
-directory. These files overwritten unconditionally on every run.
+directory. These files are overwritten unconditionally on every run.
Commit map
~~~~~~~~~~
@@ -372,7 +372,7 @@
* All commits in range of the rewrite will be listed, even commits
that are unchanged (e.g. because the commit pre-dated when the
large file(s) were introduced to the repo).
- * An all-zeros hash, or null SHA, represents a non-existant object.
+ * An all-zeros hash, or null SHA, represents a non-existent object.
When in the "new" column, this means the commit was removed
entirely.
@@ -382,9 +382,9 @@
The `.git/filter-repo/ref-map` file contains a mapping of which local
references were changed.
- * A header is the first line with the text "old" and "new"
+ * A header is the first line with the text "old", "new" and "ref"
* Reference mappings are in no particular order
- * An all-zeros hash, or null SHA, represents a non-existant object.
+ * An all-zeros hash, or null SHA, represents a non-existent object.
When in the "new" column, this means the ref was removed entirely.
[[FRESHCLONE]]
@@ -1047,7 +1047,7 @@
There are four callbacks that allow you to operate directly on raw
objects that contain data that's easy to write in
-linkgit:fast-import[1] format:
+linkgit:git-fast-import[1] format:
--------------------------------------------------
--blob-callback
@@ -1378,7 +1378,7 @@
Comments on reversibility
^^^^^^^^^^^^^^^^^^^^^^^^^
-Some people are interested in reversibility of of a rewrite; e.g. rewrite
+Some people are interested in reversibility of a rewrite; e.g. rewrite
history, possibly add some commits, then unrewrite and get the original
history back plus a few new "unrewritten" commits. Obviously this is
impossible if your rewrite involves throwing away information
@@ -1393,10 +1393,10 @@
* rewriting of commit hashes will probably be reversible, but it is
possible for rewritten abbreviated hashes to not be unique even if the
original abbreviated hashes were.
- * filter-repo defaults to several forms of unreversible rewriting that
+ * filter-repo defaults to several forms of irreversible rewriting that
you may need to turn off (e.g. the last two bullet points above or
reencoding commit messages into UTF-8); it's possible that additional
- forms of unreversible rewrites will be added in the future.
+ forms of irreversible rewrites will be added in the future.
* I assume that people use filter-repo for one-shot conversions, not
ongoing data transfers. I explicitly reserve the right to change any
API in filter-repo based on this presumption (and a comment to this
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/git-filter-repo-2.34.0/Documentation/html/git-filter-repo.html
new/git-filter-repo-2.38.0/Documentation/html/git-filter-repo.html
--- old/git-filter-repo-2.34.0/Documentation/html/git-filter-repo.html
2021-11-15 23:57:06.000000000 +0100
+++ new/git-filter-repo-2.38.0/Documentation/html/git-filter-repo.html
2022-10-10 20:10:19.000000000 +0200
@@ -1409,7 +1409,7 @@
<h2 id="_output">OUTPUT</h2>
<div class="sectionbody">
<div class="paragraph"><p>Every time filter-repo is run, files are created in
the <code>.git/filter-repo/</code>
-directory. These files overwritten unconditionally on every run.</p></div>
+directory. These files are overwritten unconditionally on every run.</p></div>
<div class="sect2">
<h3 id="_commit_map">Commit map</h3>
<div class="paragraph"><p>The <code>.git/filter-repo/commit-map</code> file
contains a mapping of how all
@@ -1434,7 +1434,7 @@
</li>
<li>
<p>
-An all-zeros hash, or null SHA, represents a non-existant object.
+An all-zeros hash, or null SHA, represents a non-existent object.
When in the "new" column, this means the commit was removed
entirely.
</p>
@@ -1448,7 +1448,7 @@
<div class="ulist"><ul>
<li>
<p>
-A header is the first line with the text "old" and "new"
+A header is the first line with the text "old", "new" and "ref"
</p>
</li>
<li>
@@ -1458,7 +1458,7 @@
</li>
<li>
<p>
-An all-zeros hash, or null SHA, represents a non-existant object.
+An all-zeros hash, or null SHA, represents a non-existent object.
When in the "new" column, this means the ref was removed entirely.
</p>
</li>
@@ -2167,7 +2167,7 @@
instead of strings.</p></div>
<div class="paragraph"><p>There are four callbacks that allow you to operate
directly on raw
objects that contain data that’s easy to write in
-<a href="fast-import.html">fast-import(1)</a> format:</p></div>
+<a href="git-fast-import.html">git-fast-import(1)</a> format:</p></div>
<div class="listingblock">
<div class="content">
<pre><code>--blob-callback
@@ -2643,7 +2643,7 @@
</div>
<div class="sect3">
<h4 id="_comments_on_reversibility">Comments on reversibility</h4>
-<div class="paragraph"><p>Some people are interested in reversibility of of a
rewrite; e.g. rewrite
+<div class="paragraph"><p>Some people are interested in reversibility of a
rewrite; e.g. rewrite
history, possibly add some commits, then unrewrite and get the original
history back plus a few new "unrewritten" commits. Obviously this is
impossible if your rewrite involves throwing away information
@@ -2672,10 +2672,10 @@
</li>
<li>
<p>
-filter-repo defaults to several forms of unreversible rewriting that
+filter-repo defaults to several forms of irreversible rewriting that
you may need to turn off (e.g. the last two bullet points above or
reencoding commit messages into UTF-8); it’s possible that additional
- forms of unreversible rewrites will be added in the future.
+ forms of irreversible rewrites will be added in the future.
</p>
</li>
<li>
@@ -2709,7 +2709,7 @@
<div id="footer">
<div id="footer-text">
Last updated
- 2021-11-09 09:48:35 PST
+ 2022-10-04 21:51:55 PDT
</div>
</div>
</body>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/git-filter-repo-2.34.0/Documentation/man1/git-filter-repo.1
new/git-filter-repo-2.38.0/Documentation/man1/git-filter-repo.1
--- old/git-filter-repo-2.34.0/Documentation/man1/git-filter-repo.1
2021-11-15 23:57:06.000000000 +0100
+++ new/git-filter-repo-2.38.0/Documentation/man1/git-filter-repo.1
2022-10-10 20:10:19.000000000 +0200
@@ -2,12 +2,12 @@
.\" Title: git-filter-repo
.\" Author: [FIXME: author] [see http://www.docbook.org/tdg5/en/html/author]
.\" Generator: DocBook XSL Stylesheets vsnapshot <http://docbook.sf.net/>
-.\" Date: 11/15/2021
+.\" Date: 10/10/2022
.\" Manual: Git Manual
-.\" Source: Git 2.34.0.dirty
+.\" Source: Git 2.38.0.dirty
.\" Language: English
.\"
-.TH "GIT\-FILTER\-REPO" "1" "11/15/2021" "Git 2\&.34\&.0\&.dirty" "Git Manual"
+.TH "GIT\-FILTER\-REPO" "1" "10/10/2022" "Git 2\&.38\&.0\&.dirty" "Git Manual"
.\" -----------------------------------------------------------------
.\" * Define some portability stuff
.\" -----------------------------------------------------------------
@@ -529,7 +529,7 @@
.RE
.SH "OUTPUT"
.sp
-Every time filter\-repo is run, files are created in the
\fB\&.git/filter\-repo/\fR directory\&. These files overwritten unconditionally
on every run\&.
+Every time filter\-repo is run, files are created in the
\fB\&.git/filter\-repo/\fR directory\&. These files are overwritten
unconditionally on every run\&.
.SS "Commit map"
.sp
The \fB\&.git/filter\-repo/commit\-map\fR file contains a mapping of how all
commits were (or were not) changed\&.
@@ -575,7 +575,7 @@
.sp -1
.IP \(bu 2.3
.\}
-An all\-zeros hash, or null SHA, represents a non\-existant object\&. When in
the "new" column, this means the commit was removed entirely\&.
+An all\-zeros hash, or null SHA, represents a non\-existent object\&. When in
the "new" column, this means the commit was removed entirely\&.
.RE
.SS "Reference map"
.sp
@@ -589,7 +589,7 @@
.sp -1
.IP \(bu 2.3
.\}
-A header is the first line with the text "old" and "new"
+A header is the first line with the text "old", "new" and "ref"
.RE
.sp
.RS 4
@@ -611,7 +611,7 @@
.sp -1
.IP \(bu 2.3
.\}
-An all\-zeros hash, or null SHA, represents a non\-existant object\&. When in
the "new" column, this means the ref was removed entirely\&.
+An all\-zeros hash, or null SHA, represents a non\-existent object\&. When in
the "new" column, this means the ref was removed entirely\&.
.RE
.SH "FRESH CLONE SAFETY CHECK AND \-\-FORCE"
.sp
@@ -1512,7 +1512,7 @@
.sp
Thus, you just need to make sure your \fIBODY\fR modifies and returns
\fIfoo\fR appropriately\&. One important thing to note for all callbacks is
that filter\-repo uses bytestrings (see
\m[blue]\fBhttps://docs\&.python\&.org/3/library/stdtypes\&.html#bytes\fR\m[])
everywhere instead of strings\&.
.sp
-There are four callbacks that allow you to operate directly on raw objects
that contain data that\(cqs easy to write in \fBfast-import\fR(1) format:
+There are four callbacks that allow you to operate directly on raw objects
that contain data that\(cqs easy to write in \fBgit-fast-import\fR(1) format:
.sp
.if n \{\
.RS 4
@@ -2297,7 +2297,7 @@
\fBComments on reversibility\fR
.RS 4
.sp
-Some people are interested in reversibility of of a rewrite; e\&.g\&. rewrite
history, possibly add some commits, then unrewrite and get the original history
back plus a few new "unrewritten" commits\&. Obviously this is impossible if
your rewrite involves throwing away information (e\&.g\&. filtering out files
or replacing several different strings with \fB***REMOVED***\fR), but may be
possible with some rewrites\&. filter\-repo is likely to be a poor fit for this
type of workflow for a few reasons:
+Some people are interested in reversibility of a rewrite; e\&.g\&. rewrite
history, possibly add some commits, then unrewrite and get the original history
back plus a few new "unrewritten" commits\&. Obviously this is impossible if
your rewrite involves throwing away information (e\&.g\&. filtering out files
or replacing several different strings with \fB***REMOVED***\fR), but may be
possible with some rewrites\&. filter\-repo is likely to be a poor fit for this
type of workflow for a few reasons:
.sp
.RS 4
.ie n \{\
@@ -2340,7 +2340,7 @@
.sp -1
.IP \(bu 2.3
.\}
-filter\-repo defaults to several forms of unreversible rewriting that you may
need to turn off (e\&.g\&. the last two bullet points above or reencoding
commit messages into UTF\-8); it\(cqs possible that additional forms of
unreversible rewrites will be added in the future\&.
+filter\-repo defaults to several forms of irreversible rewriting that you may
need to turn off (e\&.g\&. the last two bullet points above or reencoding
commit messages into UTF\-8); it\(cqs possible that additional forms of
irreversible rewrites will be added in the future\&.
.RE
.sp
.RS 4
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/git-filter-repo-2.34.0/INSTALL.md
new/git-filter-repo-2.38.0/INSTALL.md
--- old/git-filter-repo-2.34.0/INSTALL.md 2021-11-15 23:57:06.000000000
+0100
+++ new/git-filter-repo-2.38.0/INSTALL.md 2022-10-10 20:10:19.000000000
+0200
@@ -73,7 +73,7 @@
of filter-repo as a python library.
You can create this symlink to (or copy of) git-filter-repo named
- git_filter-repo.py and place it in your python site packages; `python
+ git_filter_repo.py and place it in your python site packages; `python
-c "import site; print(site.getsitepackages())"` may help you find the
appropriate location for your system. Alternatively, you can place
this file anywhere within $PYTHONPATH.
@@ -118,7 +118,7 @@
```
cp -a git-filter-repo $(git --exec-path)
- cp -a git-filter-repo.1 $(git --man-path)/man1
+ cp -a git-filter-repo.1 $(git --man-path)/man1 && mandb
cp -a git-filter-repo.html $(git --html-path)
ln -s $(git --exec-path)/git-filter-repo \
$(python -c "import site;
print(site.getsitepackages()[-1])")/git_filter_repo.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/git-filter-repo-2.34.0/Makefile
new/git-filter-repo-2.38.0/Makefile
--- old/git-filter-repo-2.34.0/Makefile 2021-11-15 23:57:06.000000000 +0100
+++ new/git-filter-repo-2.38.0/Makefile 2022-10-10 20:10:19.000000000 +0200
@@ -1,4 +1,5 @@
# A bunch of installation-related paths people can override on the command line
+DESTDIR = /
prefix = $(HOME)
bindir = $(prefix)/libexec/git-core
localedir = $(prefix)/share/locale
@@ -34,10 +35,12 @@
git show origin/docs:html/git-filter-repo.html
>Documentation/html/git-filter-repo.html
install: snag_docs #fixup_locale
- cp -a git-filter-repo "$(bindir)/"
- ln -sf "$(bindir)/git-filter-repo" "$(pythondir)/git_filter_repo.py"
- cp -a Documentation/man1/git-filter-repo.1
"$(mandir)/man1/git-filter-repo.1"
- cp -a Documentation/html/git-filter-repo.html
"$(htmldir)/git-filter-repo.html"
+ install -Dm0755 git-filter-repo "$(DESTDIR)/$(bindir)/git-filter-repo"
+ install -dm0755 "$(DESTDIR)/$(pythondir)"
+ ln -sf "$(bindir)/git-filter-repo"
"$(DESTDIR)/$(pythondir)/git_filter_repo.py"
+ install -Dm0644 Documentation/man1/git-filter-repo.1
"$(DESTDIR)/$(mandir)/man1/git-filter-repo.1"
+ install -Dm0644 Documentation/html/git-filter-repo.html
"$(DESTDIR)/$(htmldir)/git-filter-repo.html"
+ if which mandb > /dev/null; then mandb; fi
#
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/git-filter-repo-2.34.0/contrib/filter-repo-demos/README.md
new/git-filter-repo-2.38.0/contrib/filter-repo-demos/README.md
--- old/git-filter-repo-2.34.0/contrib/filter-repo-demos/README.md
2021-11-15 23:57:06.000000000 +0100
+++ new/git-filter-repo-2.38.0/contrib/filter-repo-demos/README.md
2022-10-10 20:10:19.000000000 +0200
@@ -16,6 +16,7 @@
clean-ignore |Delete files from history which match current gitignore
rules.
filter-lamely (or filter‑branch‑ish) |A nearly bug compatible
re-implementation of filter-branch (the git testsuite passes using it instead
of filter-branch), with some performance tricks to make it several times faster
(though it's still glacially slow compared to filter-repo).
bfg-ish |A re-implementation of most of BFG Repo Cleaner, with
new features and bug fixes.
+convert-svnexternals |Insert Git submodules according to SVN externals.
## Purpose
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/git-filter-repo-2.34.0/contrib/filter-repo-demos/clean-ignore
new/git-filter-repo-2.38.0/contrib/filter-repo-demos/clean-ignore
--- old/git-filter-repo-2.34.0/contrib/filter-repo-demos/clean-ignore
2021-11-15 23:57:06.000000000 +0100
+++ new/git-filter-repo-2.38.0/contrib/filter-repo-demos/clean-ignore
2022-10-10 20:10:19.000000000 +0200
@@ -18,6 +18,7 @@
import argparse
import os
import subprocess
+import sys
try:
import git_filter_repo as fr
except ImportError:
@@ -65,7 +66,14 @@
commit.file_changes = [x for x in commit.file_changes
if x.filename not in bad]
-checker = CheckIgnores()
-args = fr.FilteringOptions.default_options()
-filter = fr.RepoFilter(args, commit_callback=checker.skip_ignores)
-filter.run()
+
+def main():
+ checker = CheckIgnores()
+ args = fr.FilteringOptions.parse_args(sys.argv[1:])
+ filter = fr.RepoFilter(args, commit_callback=checker.skip_ignores)
+ filter.run()
+
+
+if __name__ == '__main__':
+ main()
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/git-filter-repo-2.34.0/contrib/filter-repo-demos/convert-svnexternals
new/git-filter-repo-2.38.0/contrib/filter-repo-demos/convert-svnexternals
--- old/git-filter-repo-2.34.0/contrib/filter-repo-demos/convert-svnexternals
1970-01-01 01:00:00.000000000 +0100
+++ new/git-filter-repo-2.38.0/contrib/filter-repo-demos/convert-svnexternals
2022-10-10 20:10:19.000000000 +0200
@@ -0,0 +1,587 @@
+#!/usr/bin/env python3
+
+"""
+This is a program that will insert Git submodules according to SVN externals
+definitions (svn:externals properties) from the original Subversion repository
+throughout the history.
+
+Information about the externals is obtained from the ".gitsvnextmodules" file
+created during SVN-to-Git conversion by SubGit (https://subgit.com/). Its
+config option "translate.externals=true" had to be used therefore.
+
+Actual modifications:
+- Insert gitlinks (mode 160000) into the tree.
+- Add .gitmodules file with relevant sections.
+- Remove sections converted to submodules from .gitsvnextmodules file
+ and delete it if empty.
+
+.gitsvnextmodules example:
+[submodule "somedir/extdir"]
+ path = somedir/extdir
+ owner = somedir
+ url = https://svn.example.com/somesvnrepo/trunk
+ revision = 1234
+ branch = /
+ fetch = :refs/remotes/git-svn
+ remote = svn
+ type = dir
+
+Resulting addition in "somedir" tree (cat-file pretty-print format):
+160000 commit 1234123412341234123412341234123412341234 extdir
+
+Resulting .gitmodules entry:
+[submodule "somedir/extdir"]
+ path = somedir/extdir
+ url = https://git.example.com/somegitrepo.git
+
+SVN-to-Git mapping file:
+Can be created from SubGit's "refs/svn/map".
+One line per mapping in following format:
+<svn url> TAB <svn rev> TAB <git url> TAB <git commit> TAB <state>
+- Leading '#' can be used for comments.
+- <svn url> must not contain a trailing slash.
+- <state> has to be "commit" to be usable, but can be "missing" if <git commit>
+ does not exist in the repository anymore. Adopted from git-cat-file output.
+Example:
+https://svn.example.com/somesvnrepo/trunk 1234
https://git.example.com/somegitrepo.git
1234123412341234123412341234123412341234 commit
+
+Features:
+- Repeatedly added/removed externals will be handled properly.
+- Externals replaced by directly added files and vice versa will be handled
+ properly.
+
+Caveats:
+- This script must NOT be run repeatedly. A second invocation would lead to a
+ different result in case the externals could only be converted partially.
+- Inconsistent SVN repositories (with failing checkout) not handled, i.e.
+ - normal directory and external with the same path
+ - external path not existing for the given revision
+- No attention was paid to non-ASCII and special characters in gitlink paths,
+ might cause problems.
+- There is no error handling for mandatory options missing in .gitsvnextmodules
+ file. The script would crash in case of such buggy files, but that shouldn't
+ happen in practice.
+
+TODO:
+- Add external files directly.
+- Alternatively add external directories directly instead of using a submodule.
+"""
+
+"""
+Please see the
+ ***** API BACKWARD COMPATIBILITY CAVEAT *****
+near the top of git-filter-repo.
+"""
+
+import argparse
+import os
+import sys
+import shutil
+import subprocess
+import configparser
+from urllib.parse import urlsplit
+
+try:
+ import git_filter_repo as fr
+except ImportError:
+ raise SystemExit("Error: Couldn't find git_filter_repo.py. Did you forget
to make a symlink to git-filter-repo named git_filter_repo.py or did you forget
to put the latter in your PYTHONPATH?")
+
+svn_root_url = ""
+svn_git_mappings = []
+
+def parse_args():
+ """
+ Parse and return arguments for this script.
+
+ Also do some argument sanity checks and adaptions.
+ """
+ parser = argparse.ArgumentParser(
+ description="Add Git submodules according to svn:externals from
.gitsvnextmodules. "
+ "As preparation for this conversion process, an analysis can
be performed.")
+
+ parser.add_argument('--force', '-f', action='store_true',
+ help="Rewrite repository history even if the current repo does not "
+ "look like a fresh clone.")
+ parser.add_argument('--refs', nargs='+',
+ help="Limit history rewriting to the specified refs. Option is directly "
+ "forwarded to git-filter-repo, see there for details and caveats. "
+ "Use for debugging purposes only!")
+ parser.add_argument('--svn-root-url',
+ help="Root URL of the corresponding SVN repository, "
+ "needed for conversion of relative to absolute external URLs.")
+
+ analysis = parser.add_argument_group(title="Analysis")
+ analysis.add_argument('--analyze', action='store_true',
+ help="Analyze repository history and create auxiliary files for
conversion process.")
+ analysis.add_argument('--report-dir', type=os.fsencode,
+ help="Directory to write report, defaults to
GIT_DIR/filter-repo/svnexternals, "
+ "refuses to run if exists, --force delete existing dir first.")
+
+ conversion = parser.add_argument_group(title="Conversion")
+ conversion.add_argument('--svn-git-mapfiles', type=os.fsencode, nargs='+',
metavar='MAPFILE',
+ help="Files with SVN-to-Git revision mappings for SVN externals
conversion.")
+
+ args = parser.parse_args()
+
+ if args.analyze and args.svn_git_mapfiles:
+ raise SystemExit("Error: --svn-git-mapfiles makes no sense with
--analyze.")
+
+ if not args.analyze and not args.svn_git_mapfiles:
+ raise SystemExit("Error: --svn-git-mapfiles is required for the conversion
process.")
+
+ return args
+
+def read_mappings(mapfiles):
+ """
+ Read files with SVN-to-Git mappings and return a list of mappings from it.
+ """
+ mappings = []
+ for mapfile in mapfiles:
+ with open(mapfile, "rb") as f:
+ for line in f:
+ line = line.rstrip(b'\r\n')
+
+ # Skip blank and comment lines
+ if not line or line.startswith(b'#'):
+ continue
+
+ # Convert to string for use with configparser later
+ line = line.decode()
+
+ # Parse the line
+ fields = line.split('\t', 4)
+ mapping = {'svn_url': fields[0],
+ 'svn_rev': int(fields[1]),
+ 'git_url': fields[2],
+ 'git_commit': fields[3],
+ 'state': fields[4]}
+
+ mappings.append(mapping)
+ return mappings
+
+cat_file_process = None
+def parse_config(blob_id):
+ """
+ Create a configparser object for a .gitsvnextmodules/.gitmodules file from
+ its blob ID.
+ """
+ parsed_config = configparser.ConfigParser()
+
+ if blob_id is not None:
+ # Get the blob contents
+ cat_file_process.stdin.write(blob_id + b'\n')
+ cat_file_process.stdin.flush()
+ objhash, objtype, objsize = cat_file_process.stdout.readline().split()
+ contents_plus_newline = cat_file_process.stdout.read(int(objsize)+1)
+
+ # Parse it
+ parsed_config.read_string(contents_plus_newline.decode())
+
+ return parsed_config
+
+def create_blob(parsed_config):
+ """
+ Create a filter-repo blob object from a .gitsvnextmodules/.gitmodules
+ configparser object according to Git config style.
+ """
+ lines = []
+ for sec in parsed_config.sections():
+ lines.append("[" + sec + "]\n")
+ for opt in parsed_config.options(sec):
+ lines.append("\t" + opt + " = " + parsed_config[sec][opt] + "\n")
+
+ return fr.Blob(''.join(lines).encode())
+
+def get_git_url(svn_url):
+ """
+ Get the Git URL for a corresponding SVN URL.
+ """
+ for entry in svn_git_mappings:
+ if entry['svn_url'] == svn_url:
+ return entry['git_url']
+ else:
+ return None
+
+def get_git_commit_hash(svn_url, svn_rev):
+ """
+ Get the Git commit hash for its corresponding SVN URL+revision.
+
+ The mapping is not restricted to the exact revision, but also uses the next
+ lower revision found. Needed when the revision was set to that of the root
+ URL instead of to that of the specific subdirectory (e.g. trunk). TortoiseSVN
+ behaves so when setting the external to HEAD.
+ """
+ ent = None
+ rev = 0
+
+ for entry in svn_git_mappings:
+ if (entry['svn_url'] == svn_url
+ and entry['svn_rev'] <= svn_rev
+ and entry['svn_rev'] > rev):
+ ent = entry
+ rev = entry['svn_rev']
+
+ if ent is not None and ent['state'] == "commit":
+ return ent['git_commit']
+ else:
+ return None
+
+def get_absolute_svn_url(svnext_url, svn_root_url):
+ """
+ Convert a relative svn:externals URL to an absolute one.
+
+ If the format is unsupported, return the URL unchanged with success=False.
+ If no root URL is given or the URL is absolute already, return it unchanged.
+
+ In all cases, even if returned "unchanged", trailing slashes are removed.
+ """
+ # Remove trailing slash(es)
+ svnext_url = svnext_url.rstrip("/")
+ svn_root_url = svn_root_url.rstrip("/")
+
+ # Normalize URLs in relative format
+ svn_root_parsed = urlsplit(svn_root_url)
+ if svnext_url.startswith(("../", "^/../")): # unsupported
+ return (False, svnext_url)
+ elif not svn_root_url:
+ pass # unchanged
+ elif svnext_url.startswith("^/"):
+ svnext_url = svn_root_url + svnext_url[1:]
+ elif svnext_url.startswith("//"):
+ svnext_url = svn_root_parsed.scheme + ":" + svnext_url
+ elif svnext_url.startswith("/"):
+ svnext_url = svn_root_parsed.scheme + "://" + svn_root_parsed.netloc +
svnext_url
+
+ return True, svnext_url
+
+def add_submodule_tree_entry(commit, parsed_config, section):
+ """
+ Add a submodule entry to the tree of a Git commit.
+
+ SVN externals information obtained from parsed .gitsvnextmodules file.
+ """
+ # Skip type=file (SVN file external), not possible as submodule
+ if parsed_config[section]['type'] != 'dir':
+ return False
+
+ success, svn_url = get_absolute_svn_url(parsed_config[section]['url'],
svn_root_url)
+ # Skip unsupported URL format
+ if not success:
+ return False
+
+ # Get SVN revision
+ if parsed_config.has_option(section, 'revision'):
+ svn_rev = int(parsed_config[section]['revision'])
+ else:
+ # TODO: revision has to be guessed according to commit timestamp, skip for
now
+ return False
+
+ # SVN url+revision mapping to Git commit
+ git_hash = get_git_commit_hash(svn_url, svn_rev)
+ # Skip missing or unusable mapping
+ if git_hash is None:
+ return False
+ git_hash = git_hash.encode()
+
+ dirname = parsed_config[section]['path'].encode()
+
+ # Add gitlink to tree
+ commit.file_changes.append(fr.FileChange(b'M', dirname, git_hash, b'160000'))
+
+ return True
+
+def get_commit_map_path():
+ """
+ Return path to commit-map file.
+ """
+ git_dir = fr.GitUtils.determine_git_dir(b'.')
+ return os.path.join(git_dir, b'filter-repo', b'commit-map')
+
+def parse_commit_map(commit_map_file):
+ """
+ Parse commit-map file and return a dictionary.
+ """
+ parsed_map = {}
+ with open(commit_map_file, "rb") as f:
+ for line in f:
+ line = line.rstrip(b'\r\n')
+
+ # Skip blank lines
+ if not line:
+ continue
+
+ # Store old/new commits, also the "old"/"new" header in the first line
+ old, new = line.split()
+ parsed_map[old] = new
+ return parsed_map
+
+def merge_commit_maps(old_commit_map, new_commit_map):
+ """
+ Merge old and new commit-map by omitting intermediate commits.
+
+ Return the merged dictionary.
+ """
+ merged_map = {}
+ for (key, old_val) in old_commit_map.items():
+ new_val = new_commit_map[old_val] if old_val in new_commit_map else old_val
+ merged_map[key] = new_val
+ return merged_map
+
+def write_commit_map(commit_map, commit_map_file):
+ """
+ Write commit-map dictionary to file.
+ """
+ with open(commit_map_file, 'wb') as f:
+ for (old, new) in commit_map.items():
+ f.write(b'%-40s %s\n' % (old, new))
+
+def create_report_dir(args):
+ """
+ Create the directory for analysis report.
+ """
+ if args.report_dir:
+ reportdir = args.report_dir
+ else:
+ git_dir = fr.GitUtils.determine_git_dir(b'.')
+
+ # Create the report directory as necessary
+ results_tmp_dir = os.path.join(git_dir, b'filter-repo')
+ if not os.path.isdir(results_tmp_dir):
+ os.mkdir(results_tmp_dir)
+ reportdir = os.path.join(results_tmp_dir, b'svnexternals')
+
+ if os.path.isdir(reportdir):
+ if args.force:
+ sys.stdout.write("Warning: Removing recursively: \"%s\"" %
fr.decode(reportdir))
+ shutil.rmtree(reportdir)
+ else:
+ sys.stdout.write("Error: dir already exists (use --force to delete):
\"%s\"\n" % fr.decode(reportdir))
+ sys.exit(1)
+
+ os.mkdir(reportdir)
+
+ return reportdir
+
+analysis = {'dir_ext_orig': [],
+ 'dir_ext_abs': [],
+ 'file_ext_orig': [],
+ 'file_ext_abs': []}
+def write_analysis(reportdir):
+ """
+ Prepare analysis and write it to files in report directory.
+ """
+ analysis['dir_ext_orig'].sort()
+ analysis['dir_ext_abs'].sort()
+ analysis['file_ext_orig'].sort()
+ analysis['file_ext_abs'].sort()
+
+ sys.stdout.write("Writing reports to %s..." % fr.decode(reportdir))
+ sys.stdout.flush()
+
+ with open(os.path.join(reportdir, b"dir-externals-original.txt"), 'wb') as f:
+ for url in analysis['dir_ext_orig']:
+ f.write(("%s\n" % url).encode())
+
+ with open(os.path.join(reportdir, b"dir-externals-absolute.txt"), 'wb') as f:
+ for url in analysis['dir_ext_abs']:
+ f.write(("%s\n" % url).encode())
+
+ with open(os.path.join(reportdir, b"file-externals-original.txt"), 'wb') as
f:
+ for url in analysis['file_ext_orig']:
+ f.write(("%s\n" % url).encode())
+
+ with open(os.path.join(reportdir, b"file-externals-absolute.txt"), 'wb') as
f:
+ for url in analysis['file_ext_abs']:
+ f.write(("%s\n" % url).encode())
+
+ sys.stdout.write("done.\n")
+
+def analyze_externals(commit, metadata):
+ """
+ Generate/extend analysis of SVN externals for a Git commit.
+
+ Used as filter-repo commit callback.
+ """
+ for change in commit.file_changes:
+ if change.filename == b'.gitsvnextmodules' and change.type == b'M':
+ gitsvnextmodules = parse_config(change.blob_id)
+
+ for sec in gitsvnextmodules.sections():
+ url = gitsvnextmodules[sec]['url']
+ success, abs_url = get_absolute_svn_url(url, svn_root_url)
+
+ # List of svn:externals URLs, also add the URL to the absolute list if
+ # conversion was not successful
+ if gitsvnextmodules[sec]['type'] == 'dir':
+ if url not in analysis['dir_ext_orig']:
+ analysis['dir_ext_orig'].append(url)
+ if abs_url not in analysis['dir_ext_abs']:
+ analysis['dir_ext_abs'].append(abs_url)
+ else:
+ if url not in analysis['file_ext_orig']:
+ analysis['file_ext_orig'].append(url)
+ if abs_url not in analysis['file_ext_abs']:
+ analysis['file_ext_abs'].append(abs_url)
+
+def insert_submodules(commit, metadata):
+ """
+ Insert submodules for a Git commit.
+
+ Used as filter-repo commit callback.
+
+ Since .gitsvnextmodules just contains the svn:externals state for the given
+ commit, we cannot derive specific changes from that file.
+ So we can only add/modify the gitlinks according to .gitsvnextmodules
+ (without knowing whether adding a new or modifying an existing or even
+ "modifying" an unchanged submodule, but none of that really matters).
+ We do not have information about deleted externals, those will be handled in
+ a separate filter run afterwards.
+
+ The .gitmodules file however will already be correct in this function because
+ we don't need to know about specific changes to add, modify or delete it.
+ """
+ for change in commit.file_changes:
+ if change.filename == b'.gitsvnextmodules' and change.type in (b'M', b'D'):
+ gitsvnextmodules = parse_config(change.blob_id)
+ gitmodules = configparser.ConfigParser()
+
+ # Add gitlinks to the tree and prepare .gitmodules file content
+ for sec in gitsvnextmodules.sections():
+ if add_submodule_tree_entry(commit, gitsvnextmodules, sec):
+ # Gitlink added
+ # -> Add this entry to .gitmodules as well
+
+ # Create the section name string manually, do not rely on
+ # .gitsvnextmodules to always use the proper section name.
+ sec_name = 'submodule "' + gitsvnextmodules[sec]['path'] + '"'
+ gitmodules[sec_name] = {}
+
+ # submodule.<name>.path
+ gitmodules[sec_name]['path'] = gitsvnextmodules[sec]['path']
+
+ # submodule.<name>.url
+ success, svn_url =
get_absolute_svn_url(gitsvnextmodules[sec]['url'], svn_root_url)
+ git_url = get_git_url(svn_url)
+ if git_url is not None:
+ gitmodules[sec_name]['url'] = git_url
+ else:
+ # Abort, but this will not happen in practice, catched in
+ # add_submodule_tree_entry() via get_git_commit_hash() already.
+ raise SystemExit("Error: No Git URL found in mapping although a
commit hash could be found.")
+
+ # Write blob and adapt tree for .gitmodules
+ if gitmodules.sections():
+ # Create a blob object from the content and add it to the tree.
+ blob = create_blob(gitmodules)
+ filter.insert(blob)
+ commit.file_changes.append(fr.FileChange(b'M', b'.gitmodules',
blob.id, b'100644'))
+ else:
+ # Delete the file, even if a "git rm" of all submodules keeps it empty.
+ commit.file_changes.append(fr.FileChange(b'D', b'.gitmodules'))
+
+def delete_submodules(commit, metadata):
+ """
+ Delete submodules from a Git commit.
+
+ Used as filter-repo commit callback.
+
+ Delete all submodules (inserted in the previous filter run) without an entry
+ in .gitsvnextmodules, these were real deletions of externals, which couldn't
+ be detected before.
+ Only the tree entries have to be removed because the .gitmodules file is
+ already in correct state from previous filter run.
+ """
+ for change in commit.file_changes:
+ if change.filename == b'.gitsvnextmodules' and change.type in (b'M', b'D'):
+ gitsvnextmodules = parse_config(change.blob_id)
+
+ # Search for all submodules in the tree
+ output = subprocess.check_output('git ls-tree -d -r -z'.split() +
[commit.original_id])
+ for line in output.split(b'\x00'):
+ if not line:
+ continue
+ mode_objtype_objid, dirname = line.split(b'\t', 1)
+ mode, objtype, objid = mode_objtype_objid.split(b' ')
+ if mode == b'160000' and objtype == b'commit':
+ # Submodule found
+ # -> Delete it if there is no corresponding entry in
+ # .gitsvnextmodules, keep/reinsert it otherwise
+ for sec in gitsvnextmodules.sections():
+ if gitsvnextmodules[sec]['path'].encode() == dirname:
+ # Reinsert it, might have been deleted in previous commits
+ if add_submodule_tree_entry(commit, gitsvnextmodules, sec):
+ # And remove the config section because this external has been
+ # converted
+ gitsvnextmodules.remove_section(sec)
+ break
+ else:
+ # Delete it
+ commit.file_changes.append(fr.FileChange(b'D', dirname))
+
+ # Rewrite .gitsvnextmodules to contain the unhandled externals only,
+ # delete it if empty (all externals converted).
+ if gitsvnextmodules.sections():
+ # Create a blob object from the content and replace the original one.
+ blob = create_blob(gitsvnextmodules)
+ filter.insert(blob)
+ change.blob_id = blob.id
+ else:
+ if change.type == b'M':
+ # File became empty, delete it
+ commit.file_changes.append(fr.FileChange(b'D', b'.gitsvnextmodules'))
+ break # avoid endless for loop
+ #else:
+ # File was empty already, delete command already present in stream
+
+my_args = parse_args()
+
+# Use passed URL without trailing slash(es)
+if my_args.svn_root_url:
+ svn_root_url = my_args.svn_root_url.rstrip("/")
+
+# Arguments forwarded to filter-repo
+extra_args = []
+if my_args.force:
+ extra_args = ['--force']
+if my_args.refs:
+ extra_args += ['--refs'] + my_args.refs
+
+cat_file_process = subprocess.Popen(['git', 'cat-file', '--batch'],
+ stdin = subprocess.PIPE,
+ stdout = subprocess.PIPE)
+if my_args.analyze:
+ # Analysis
+ reportdir = create_report_dir(my_args)
+
+ fr_args = fr.FilteringOptions.parse_args(['--dry-run']
+ + extra_args)
+ filter = fr.RepoFilter(fr_args, commit_callback=analyze_externals)
+ filter.run()
+
+ write_analysis(reportdir)
+else:
+ # Conversion
+ svn_git_mappings = read_mappings(my_args.svn_git_mapfiles)
+
+ # There are no references to commit hashes in commit messages because this
+ # script runs on a Git repository converted from a Subversion repository.
+ fr_args = fr.FilteringOptions.parse_args(['--preserve-commit-hashes',
+ '--preserve-commit-encoding',
+ '--replace-refs', 'update-no-add']
+ + extra_args)
+ filter = fr.RepoFilter(fr_args, commit_callback=insert_submodules)
+ filter.run()
+
+ # Store commit-map after first run
+ first_commit_map = parse_commit_map(get_commit_map_path())
+
+ filter = fr.RepoFilter(fr_args, commit_callback=delete_submodules)
+ filter.run()
+
+ # Update commit-map after second run, based on original IDs
+ second_commit_map = parse_commit_map(get_commit_map_path())
+ merged_commit_map = merge_commit_maps(first_commit_map, second_commit_map)
+ write_commit_map(merged_commit_map, get_commit_map_path())
+
+cat_file_process.stdin.close()
+cat_file_process.wait()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/git-filter-repo-2.34.0/contrib/filter-repo-demos/lint-history
new/git-filter-repo-2.38.0/contrib/filter-repo-demos/lint-history
--- old/git-filter-repo-2.34.0/contrib/filter-repo-demos/lint-history
2021-11-15 23:57:06.000000000 +0100
+++ new/git-filter-repo-2.38.0/contrib/filter-repo-demos/lint-history
2022-10-10 20:10:19.000000000 +0200
@@ -95,6 +95,10 @@
"random name. If the linting program needs to know the file "
"basename to operate correctly (e.g. because it needs to know "
"the file's extension), then pass this argument"))
+parser.add_argument('--refs', nargs='+',
+ help=("Limit history rewriting to the specified refs. "
+ "Implies --partial of git-filter-repo (and all its "
+ "implications)."))
parser.add_argument('command', nargs=argparse.REMAINDER,
help=("Lint command to run, other than the filename at the end"))
lint_args = parser.parse_args()
@@ -156,7 +160,10 @@
exec('def is_relevant(filename):\n '+'\n '.join(body.splitlines()),
globals())
lint_args.filenames_important = True
-args = fr.FilteringOptions.default_options()
+input_args = []
+if lint_args.refs:
+ input_args = ["--args",] + lint_args.refs
+args = fr.FilteringOptions.parse_args(input_args, error_on_empty = False)
args.force = True
if lint_args.filenames_important:
tmpdir = tempfile.mkdtemp().encode()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/git-filter-repo-2.34.0/git-filter-repo
new/git-filter-repo-2.38.0/git-filter-repo
--- old/git-filter-repo-2.34.0/git-filter-repo 2021-11-15 23:57:06.000000000
+0100
+++ new/git-filter-repo-2.38.0/git-filter-repo 2022-10-10 20:10:19.000000000
+0200
@@ -320,8 +320,8 @@
for old, new in self.changes.items():
old_name, old_email = old
new_name, new_email = new
- if (not old_email or email.lower() == old_email.lower()) and (
- name == old_name or not old_name):
+ if (old_email is None or email.lower() == old_email.lower()) and (
+ name == old_name or not old_name):
return (new_name or name, new_email or email)
return (name, email)
@@ -941,7 +941,7 @@
# Compile some regexes and cache those
self._mark_re = re.compile(br'mark :(\d+)\n$')
self._parent_regexes = {}
- parent_regex_rules = (b' :(\d+)\n$', b' ([0-9a-f]{40})\n')
+ parent_regex_rules = (br' :(\d+)\n$', br' ([0-9a-f]{40})\n')
for parent_refname in (b'from', b'merge'):
ans = [re.compile(parent_refname+x) for x in parent_regex_rules]
self._parent_regexes[parent_refname] = ans
@@ -3811,6 +3811,7 @@
batch_check_process = None
batch_check_output_re = re.compile(b'^([0-9a-f]{40}) ([a-z]+) ([0-9]+)$')
with open(os.path.join(metadata_dir, b'ref-map'), 'bw') as f:
+ f.write(("%-40s %-40s %s\n" % (_("old"), _("new"), _("ref"))).encode())
for refname, old_hash in orig_refs.items():
if refname not in exported_refs:
continue
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/git-filter-repo-2.34.0/t/t9390/less-empty-keepme
new/git-filter-repo-2.38.0/t/t9390/less-empty-keepme
--- old/git-filter-repo-2.34.0/t/t9390/less-empty-keepme 2021-11-15
23:57:06.000000000 +0100
+++ new/git-filter-repo-2.38.0/t/t9390/less-empty-keepme 2022-10-10
20:10:19.000000000 +0200
@@ -2,33 +2,33 @@
reset refs/heads/master
commit refs/heads/master
mark :1
-author Full Name <[email protected]> 1000020000 +0100
-committer Full Name <[email protected]> 1000020000 +0100
+author Full Name <[email protected]> 1000000000 +0100
+committer Full Name <[email protected]> 1000000000 +0100
data 2
-C
+A
commit refs/heads/master
mark :2
-author Full Name <[email protected]> 1000030000 +0100
-committer Full Name <[email protected]> 1000030000 +0100
+author Full Name <[email protected]> 1000010000 +0100
+committer Full Name <[email protected]> 1000010000 +0100
data 2
-D
+B
from :1
reset refs/heads/master
commit refs/heads/master
mark :3
-author Full Name <[email protected]> 1000000000 +0100
-committer Full Name <[email protected]> 1000000000 +0100
+author Full Name <[email protected]> 1000020000 +0100
+committer Full Name <[email protected]> 1000020000 +0100
data 2
-A
+C
commit refs/heads/master
mark :4
-author Full Name <[email protected]> 1000010000 +0100
-committer Full Name <[email protected]> 1000010000 +0100
+author Full Name <[email protected]> 1000030000 +0100
+committer Full Name <[email protected]> 1000030000 +0100
data 2
-B
+D
from :3
blob
@@ -42,8 +42,8 @@
committer Full Name <[email protected]> 1000040000 +0100
data 29
E: Merge commit 'D' into 'B'
-from :4
-merge :2
+from :2
+merge :4
M 100644 :5 keepme
commit refs/heads/master
@@ -68,8 +68,8 @@
committer Full Name <[email protected]> 1000050000 +0100
data 29
F: Merge commit 'D' into 'B'
-from :4
-merge :2
+from :2
+merge :4
blob
mark :10
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/git-filter-repo-2.34.0/t/t9390/more-empty-keepme
new/git-filter-repo-2.38.0/t/t9390/more-empty-keepme
--- old/git-filter-repo-2.34.0/t/t9390/more-empty-keepme 2021-11-15
23:57:06.000000000 +0100
+++ new/git-filter-repo-2.38.0/t/t9390/more-empty-keepme 2022-10-10
20:10:19.000000000 +0200
@@ -2,29 +2,29 @@
blob
mark :1
data 10
-keepme v2
+keepme v1
reset refs/heads/master
commit refs/heads/master
mark :2
-author Full Name <[email protected]> 1000080000 +0100
-committer Full Name <[email protected]> 1000080000 +0100
-data 2
-I
+author Full Name <[email protected]> 1000040000 +0100
+committer Full Name <[email protected]> 1000040000 +0100
+data 29
+E: Merge commit 'D' into 'B'
M 100644 :1 keepme
blob
mark :3
data 10
-keepme v1
+keepme v2
reset refs/heads/master
commit refs/heads/master
mark :4
-author Full Name <[email protected]> 1000040000 +0100
-committer Full Name <[email protected]> 1000040000 +0100
-data 29
-E: Merge commit 'D' into 'B'
+author Full Name <[email protected]> 1000080000 +0100
+committer Full Name <[email protected]> 1000080000 +0100
+data 2
+I
M 100644 :3 keepme
commit refs/heads/master
@@ -33,7 +33,7 @@
committer Full Name <[email protected]> 1000090000 +0100
data 29
J: Merge commit 'I' into 'H'
-from :4
-merge :2
+from :2
+merge :4
done