From 2a9dd53d143fd442ada8c4fb51e8e39ffd2f1b0f Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 13 Nov 2019 15:47:05 -0800 Subject: [PATCH 1/6] Show all unnamed heads at once Co-Authored-By: ostan89@gmail.com --- hg-fast-export.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hg-fast-export.py b/hg-fast-export.py index ed9fad0..1ccb4f9 100755 --- a/hg-fast-export.py +++ b/hg-fast-export.py @@ -497,16 +497,17 @@ def verify_heads(ui,repo,cache,force,branchesmap): # verify that branch has exactly one head t={} + unnamed_heads=False for h in repo.filtered(b'visible').heads(): (_,_,_,_,_,_,branch,_)=get_changeset(ui,repo,h) if t.get(branch,False): stderr_buffer.write( - b'Error: repository has at least one unnamed head: hg r%d\n' + b'Error: repository has an unnamed head: hg r%d\n' % repo.changelog.rev(h) ) - if not force: return False + unnamed_heads=True t[branch]=True - + if unnamed_heads and not force: return False return True def hg2git(repourl,m,marksfile,mappingfile,headsfile,tipfile, From 50631c4b3450865139f23ca29042567dd652c419 Mon Sep 17 00:00:00 2001 From: Ondrej Stanek Date: Fri, 31 Jul 2020 10:30:53 +0200 Subject: [PATCH 2/6] Add option --ignore-unnamed-heads This option allows the user to ignore only unnamed heads (compared to --force which ignores all non-fatal issues). The intended use is for a future plugin converting unnamed heads to named branches. --- hg-fast-export.py | 17 +++++++++++------ hg-fast-export.sh | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/hg-fast-export.py b/hg-fast-export.py index 1ccb4f9..ea6c6ac 100755 --- a/hg-fast-export.py +++ b/hg-fast-export.py @@ -475,7 +475,7 @@ def branchtip(repo, heads): break return tip -def verify_heads(ui,repo,cache,force,branchesmap): +def verify_heads(ui,repo,cache,force,ignore_unnamed_heads,branchesmap): branches={} for bn, heads in repo.branchmap().iteritems(): branches[bn] = branchtip(repo, heads) @@ -506,13 +506,14 @@ def verify_heads(ui,repo,cache,force,branchesmap): % repo.changelog.rev(h) ) unnamed_heads=True + if not force and not ignore_unnamed_heads: return False t[branch]=True - if unnamed_heads and not force: return False + if unnamed_heads and not force and not ignore_unnamed_heads: return False return True def hg2git(repourl,m,marksfile,mappingfile,headsfile,tipfile, authors={},branchesmap={},tagsmap={}, - sob=False,force=False,hgtags=False,notes=False,encoding='',fn_encoding='', + sob=False,force=False,ignore_unnamed_heads=False,hgtags=False,notes=False,encoding='',fn_encoding='', plugins={}): def check_cache(filename, contents): if len(contents) == 0: @@ -533,7 +534,7 @@ def hg2git(repourl,m,marksfile,mappingfile,headsfile,tipfile, ui,repo=setup_repo(repourl) - if not verify_heads(ui,repo,heads_cache,force,branchesmap): + if not verify_heads(ui,repo,heads_cache,force,ignore_unnamed_heads,branchesmap): return 1 try: @@ -619,7 +620,9 @@ if __name__=='__main__': parser.add_option("-T","--tags",dest="tagsfile", help="Read tags map from TAGSFILE") parser.add_option("-f","--force",action="store_true",dest="force", - default=False,help="Ignore validation errors by force") + default=False,help="Ignore validation errors by force, implies --ignore-unnamed-heads") + parser.add_option("--ignore-unnamed-heads",action="store_true",dest="ignore_unnamed_heads", + default=False,help="Ignore unnamed head errors") parser.add_option("-M","--default-branch",dest="default_branch", help="Set the default branch") parser.add_option("-o","--origin",dest="origin_name", @@ -715,6 +718,8 @@ if __name__=='__main__': sys.exit(hg2git(options.repourl,m,options.marksfile,options.mappingfile, options.headsfile, options.statusfile, authors=a,branchesmap=b,tagsmap=t, - sob=options.sob,force=options.force,hgtags=options.hgtags, + sob=options.sob,force=options.force, + ignore_unnamed_heads=options.ignore_unnamed_heads, + hgtags=options.hgtags, notes=options.notes,encoding=encoding,fn_encoding=fn_encoding, plugins=plugins_dict)) diff --git a/hg-fast-export.sh b/hg-fast-export.sh index 6533452..fd0ff53 100755 --- a/hg-fast-export.sh +++ b/hg-fast-export.sh @@ -45,7 +45,7 @@ if [ -z "${PYTHON}" ]; then exit 1 fi -USAGE="[--quiet] [-r ] [--force] [-m ] [-s] [--hgtags] [-A ] [-B ] [-T ] [-M ] [-o ] [--hg-hash] [-e ]" +USAGE="[--quiet] [-r ] [--force] [--ignore-unnamed-heads] [-m ] [-s] [--hgtags] [-A ] [-B ] [-T ] [-M ] [-o ] [--hg-hash] [-e ]" LONG_USAGE="Import hg repository up to either tip or If is omitted, use last hg repository as obtained from state file, GIT_DIR/$PFX-$SFX_STATE by default. From 5c1cbf82b04e29a231b54a027c7f98e43bae1da4 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 13 Nov 2019 15:22:15 -0800 Subject: [PATCH 3/6] Add revision to commit_data for commit plugins Co-Authored-By: ostan89@gmail.com --- README.md | 2 +- hg-fast-export.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31610a8..46c3fa0 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ defined filter methods in the [dos2unix](./plugins/dos2unix) and [branch_name_in_commit](./plugins/branch_name_in_commit) plugins. ``` -commit_data = {'branch': branch, 'parents': parents, 'author': author, 'desc': desc} +commit_data = {'branch': branch, 'parents': parents, 'author': author, 'desc': desc, 'revision': revision} def commit_message_filter(self,commit_data): ``` diff --git a/hg-fast-export.py b/hg-fast-export.py index ea6c6ac..d830296 100755 --- a/hg-fast-export.py +++ b/hg-fast-export.py @@ -304,7 +304,7 @@ def export_commit(ui,repo,revision,old_marks,max,count,authors, author = get_author(desc,user,authors) if plugins and plugins['commit_message_filters']: - commit_data = {'branch': branch, 'parents': parents, 'author': author, 'desc': desc} + commit_data = {'branch': branch, 'parents': parents, 'author': author, 'desc': desc, 'revision': revision} for filter in plugins['commit_message_filters']: filter(commit_data) branch = commit_data['branch'] From 21827a53f7bb40904bc453470e82f33fc4ff4284 Mon Sep 17 00:00:00 2001 From: Ethan Furman Date: Wed, 13 Nov 2019 16:19:19 -0800 Subject: [PATCH 4/6] Add head2branch plugin Support converting unnamed heads to named branches during mercurial conversions. Co-Authored-By: ostan89@gmail.com --- README.md | 2 ++ plugins/head2branch/README.md | 12 ++++++++++++ plugins/head2branch/__init__.py | 23 +++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 plugins/head2branch/README.md create mode 100644 plugins/head2branch/__init__.py diff --git a/README.md b/README.md index 46c3fa0..3fb2474 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,8 @@ exactly one head each. Otherwise commits to the tip of these heads within the branch will get flattened into merge commits. Chris J Billington's [hg-export-tool] can help you to handle branches with duplicate heads. +Alternatively, you can use the [head2branch plugin](./plugins/head2branch) +to create a new named branch from an unnamed head. hg-fast-export will ignore any files or directories tracked by mercurial called `.git`, and will print a warning if it encounters one. Git cannot diff --git a/plugins/head2branch/README.md b/plugins/head2branch/README.md new file mode 100644 index 0000000..495166f --- /dev/null +++ b/plugins/head2branch/README.md @@ -0,0 +1,12 @@ +## Convert Head to Branch + +`fast-export` can only handle one head per branch. This plugin makes it possible +to create a new branch from a head by specifying the new branch name and +the first divergent commit for that head. The revision number for the commit +should be in decimal form. + +Note: you must run `fast-export` with `--ignore-unnamed-heads` option, +otherwise, the conversion will fail. + +To use the plugin, add the command line flag `--plugin head2branch=name,`. +The flag can be given multiple times to name more than one head. diff --git a/plugins/head2branch/__init__.py b/plugins/head2branch/__init__.py new file mode 100644 index 0000000..54e6785 --- /dev/null +++ b/plugins/head2branch/__init__.py @@ -0,0 +1,23 @@ +import sys + +def build_filter(args): + return Filter(args) + +class Filter: + + def __init__(self, args): + args = args.split(',') + self.branch_name = args[0] + self.starting_commit = int(args[1]) + self.branch_parents = set() + + def commit_message_filter(self, commit_data): + rev = commit_data['revision'] + rev_parents = commit_data['parents'] + if (rev == self.starting_commit + or any(rp in self.branch_parents for rp in rev_parents) + ): + self.branch_parents.add(rev) + commit_data['branch'] = self.branch_name.encode('ascii', 'replace') + sys.stderr.write('\nchanging r%s to branch %r\n' % (rev, self.branch_name)) + sys.stderr.flush() From 9c6dea9fd46d61b0a14a506a9b217c9b236b02fc Mon Sep 17 00:00:00 2001 From: Ondrej Stanek Date: Wed, 24 Jun 2020 13:50:27 +0200 Subject: [PATCH 5/6] Pass original hg commit hash to plugins --- README.md | 2 +- hg-fast-export.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3fb2474..2346074 100644 --- a/README.md +++ b/README.md @@ -167,7 +167,7 @@ defined filter methods in the [dos2unix](./plugins/dos2unix) and [branch_name_in_commit](./plugins/branch_name_in_commit) plugins. ``` -commit_data = {'branch': branch, 'parents': parents, 'author': author, 'desc': desc, 'revision': revision} +commit_data = {'branch': branch, 'parents': parents, 'author': author, 'desc': desc, 'revision': revision, 'hg_hash': hg_hash} def commit_message_filter(self,commit_data): ``` diff --git a/hg-fast-export.py b/hg-fast-export.py index d830296..4627eb1 100755 --- a/hg-fast-export.py +++ b/hg-fast-export.py @@ -302,9 +302,10 @@ def export_commit(ui,repo,revision,old_marks,max,count,authors, parents = [p for p in repo.changelog.parentrevs(revision) if p >= 0] author = get_author(desc,user,authors) + hg_hash=revsymbol(repo,b"%d" % revision).hex() if plugins and plugins['commit_message_filters']: - commit_data = {'branch': branch, 'parents': parents, 'author': author, 'desc': desc, 'revision': revision} + commit_data = {'branch': branch, 'parents': parents, 'author': author, 'desc': desc, 'revision': revision, 'hg_hash': hg_hash} for filter in plugins['commit_message_filters']: filter(commit_data) branch = commit_data['branch'] From a7955bc49b710c48789e3a52e566da895454d8ed Mon Sep 17 00:00:00 2001 From: Ondrej Stanek Date: Fri, 31 Jul 2020 10:38:24 +0200 Subject: [PATCH 6/6] Update head2branch plugin to accept hg commit hash The revision number isn't a unique identifier of commits across repository clones and forks, while the hg hash is guaranteed to be stable. --- plugins/head2branch/README.md | 7 ++++--- plugins/head2branch/__init__.py | 9 +++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/plugins/head2branch/README.md b/plugins/head2branch/README.md index 495166f..932d98d 100644 --- a/plugins/head2branch/README.md +++ b/plugins/head2branch/README.md @@ -2,11 +2,12 @@ `fast-export` can only handle one head per branch. This plugin makes it possible to create a new branch from a head by specifying the new branch name and -the first divergent commit for that head. The revision number for the commit -should be in decimal form. +the first divergent commit for that head. + +Note: the hg hash must be in the full form, 40 hexadecimal characters. Note: you must run `fast-export` with `--ignore-unnamed-heads` option, otherwise, the conversion will fail. -To use the plugin, add the command line flag `--plugin head2branch=name,`. +To use the plugin, add the command line flag `--plugin head2branch=name,`. The flag can be given multiple times to name more than one head. diff --git a/plugins/head2branch/__init__.py b/plugins/head2branch/__init__.py index 54e6785..3ee9f40 100644 --- a/plugins/head2branch/__init__.py +++ b/plugins/head2branch/__init__.py @@ -7,17 +7,18 @@ class Filter: def __init__(self, args): args = args.split(',') - self.branch_name = args[0] - self.starting_commit = int(args[1]) + self.branch_name = args[0].encode('ascii', 'replace') + self.starting_commit_hash = args[1].encode('ascii', 'strict') self.branch_parents = set() def commit_message_filter(self, commit_data): + hg_hash = commit_data['hg_hash'] rev = commit_data['revision'] rev_parents = commit_data['parents'] - if (rev == self.starting_commit + if (hg_hash == self.starting_commit_hash or any(rp in self.branch_parents for rp in rev_parents) ): self.branch_parents.add(rev) - commit_data['branch'] = self.branch_name.encode('ascii', 'replace') + commit_data['branch'] = self.branch_name sys.stderr.write('\nchanging r%s to branch %r\n' % (rev, self.branch_name)) sys.stderr.flush()