Git development
 help / color / mirror / Atom feed
* [PATCH 1/2] pickaxe: hoist empty needle check
From: Jeff King @ 2012-10-28 12:46 UTC (permalink / raw)
  To: Peter Oberndorfer; +Cc: git, Junio C Hamano
In-Reply-To: <20121028124540.GF11434@sigill.intra.peff.net>

If we are given an empty pickaxe needle like "git log -S ''",
it is impossible for us to find anything (because no matter
what the content, the count will always be 0). We currently
check this at the lowest level of contains(). Let's hoist
the logic much earlier to has_changes(), so that it is
simpler to return our answer before loading any blob data.

Signed-off-by: Jeff King <peff@peff.net>
---
 diffcore-pickaxe.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/diffcore-pickaxe.c b/diffcore-pickaxe.c
index a209376..61f628c 100644
--- a/diffcore-pickaxe.c
+++ b/diffcore-pickaxe.c
@@ -163,8 +163,6 @@ static unsigned int contains(struct diff_filespec *one, struct diff_options *o,
 	unsigned int cnt;
 	unsigned long sz;
 	const char *data;
-	if (!o->pickaxe[0])
-		return 0;
 	if (diff_populate_filespec(one, 0))
 		return 0;
 
@@ -206,6 +204,9 @@ static int has_changes(struct diff_filepair *p, struct diff_options *o,
 static int has_changes(struct diff_filepair *p, struct diff_options *o,
 		       regex_t *regexp, kwset_t kws)
 {
+	if (!o->pickaxe[0])
+		return 0;
+
 	if (!DIFF_FILE_VALID(p->one)) {
 		if (!DIFF_FILE_VALID(p->two))
 			return 0; /* ignore unmerged */
-- 
1.8.0.3.g3456896

^ permalink raw reply related

* [PATCH 0/2] textconv support for "log -S"
From: Jeff King @ 2012-10-28 12:45 UTC (permalink / raw)
  To: Peter Oberndorfer; +Cc: git, Junio C Hamano
In-Reply-To: <20121028120104.GE11434@sigill.intra.peff.net>

On Sun, Oct 28, 2012 at 08:01:04AM -0400, Jeff King wrote:

> -G operates on the munged data; you can see it feed the munged data to
> xdiff in diff_grep. But the optimization for handling added and removed
> files accidentally fed the wrong pointer. Fixing that is a no-brainer,
> since the optimization is inconsistent with the regular code path.
> 
> -S, however, predates the invention of textconv and has never used it.
> It is a little less clear that textconv is the right thing here, because
> it is not about grepping the diff, but about counting occurrences of the
> string inside the file content. I tend to think that doing so on the
> textconv'd data would be what people generally want, but it is a
> behavior change.

I prepared the earlier bugfix for "-G" for maint. Modifying "-S" would
be a separate feature topic, and would look like this (I built it on top
of the bugfix patch, since the tests are a follow-on).

  [1/2]: pickaxe: hoist empty needle check
  [2/2]: pickaxe: use textconv for -S counting

-Peff

^ permalink raw reply

* Re: crash on git diff-tree -Ganything <tree> for new files with textconv filter
From: Jeff King @ 2012-10-28 12:01 UTC (permalink / raw)
  To: Peter Oberndorfer; +Cc: git, Junio C Hamano
In-Reply-To: <508C29E4.5000801@arcor.de>

On Sat, Oct 27, 2012 at 08:37:24PM +0200, Peter Oberndorfer wrote:

> It seems "git diff-tree -Ganything <tree>" crashes[1] with a null
> pointer dereference
> when run on a commit that adds a file (pdf) with a textconv filter.
> 
> It can be reproduced with vanilla git by having a commit on top that
> adds a file with a textconv filter and executing git diff-tree
> -Ganything HEAD
> But running git log -Ganything still works without a crash.
> This problem seems to exist since the feature was first added in f506b8e8b5.

Thanks for a thorough bug report. I didn't reproduce the crash, but I
can see how it happens (it happens with diff-tree because we will reuse
the working tree file in that instance; it could also happen if you
turned on textconv caching).

> While testing I also noticed the -S and -G act on the original file
> instead of the textconv munged data.
> Is this intentional or caused by accessing the wrong data?

Both, perhaps. :)

-G operates on the munged data; you can see it feed the munged data to
xdiff in diff_grep. But the optimization for handling added and removed
files accidentally fed the wrong pointer. Fixing that is a no-brainer,
since the optimization is inconsistent with the regular code path.

-S, however, predates the invention of textconv and has never used it.
It is a little less clear that textconv is the right thing here, because
it is not about grepping the diff, but about counting occurrences of the
string inside the file content. I tend to think that doing so on the
textconv'd data would be what people generally want, but it is a
behavior change.

> Wild guess: should we really access p->one->data and not mf1.ptr?

Precisely. Thanks for your wild guess; it made finding the bug very
easy. :)

> Is there some more information i should provide?

The patch below should fix it. I added tests, but please try your
real-world test case on it to double-check.

-- >8 --
Subject: [PATCH] diff_grep: use textconv buffers for add/deleted files

If you use "-G" to grep a diff, we will apply a configured
textconv filter to the data before generating the diff.
However, if the diff is an addition or deletion, we do not
bother running the diff at all, and just look for the token
in the added (or removed) content. This works because we
know that the diff must contain every line of content.

However, while we used the textconv-derived buffers in the
regular diff, we accidentally passed the original unmodified
buffers to regexec when checking the added or removed
content. This could lead to an incorrect answer.

Worse, in some cases we might have a textconv buffer but no
original buffer (e.g., if we pulled the textconv data from
cache, or if we reused a working tree file when generating
it). In that case, we could actually feed NULL to regexec
and segfault.

Reported-by: Peter Oberndorfer <kumbayo84@arcor.de>
Signed-off-by: Jeff King <peff@peff.net>
---
 diffcore-pickaxe.c       |  4 ++--
 t/t4030-diff-textconv.sh | 12 ++++++++++++
 2 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/diffcore-pickaxe.c b/diffcore-pickaxe.c
index ed23eb4..a209376 100644
--- a/diffcore-pickaxe.c
+++ b/diffcore-pickaxe.c
@@ -104,10 +104,10 @@ static int diff_grep(struct diff_filepair *p, struct diff_options *o,
 		if (!mf2.ptr)
 			return 0; /* ignore unmerged */
 		/* created "two" -- does it have what we are looking for? */
-		hit = !regexec(regexp, p->two->data, 1, &regmatch, 0);
+		hit = !regexec(regexp, mf2.ptr, 1, &regmatch, 0);
 	} else if (!mf2.ptr) {
 		/* removed "one" -- did it have what we are looking for? */
-		hit = !regexec(regexp, p->one->data, 1, &regmatch, 0);
+		hit = !regexec(regexp, mf1.ptr, 1, &regmatch, 0);
 	} else {
 		/*
 		 * We have both sides; need to run textual diff and see if
diff --git a/t/t4030-diff-textconv.sh b/t/t4030-diff-textconv.sh
index eebb1ee..461d27a 100755
--- a/t/t4030-diff-textconv.sh
+++ b/t/t4030-diff-textconv.sh
@@ -84,6 +84,18 @@ test_expect_success 'status -v produces text' '
 	git reset --soft HEAD@{1}
 '
 
+test_expect_success 'grep-diff (-G) operates on textconv data (add)' '
+	echo one >expect &&
+	git log --root --format=%s -G0 >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'grep-diff (-G) operates on textconv data (modification)' '
+	echo two >expect &&
+	git log --root --format=%s -G1 >actual &&
+	test_cmp expect actual
+'
+
 cat >expect.stat <<'EOF'
  file | Bin 2 -> 4 bytes
  1 file changed, 0 insertions(+), 0 deletions(-)
-- 
1.8.0.3.g3456896

^ permalink raw reply related

* Re: merge --no-commit not able to report stats more verbosely?
From: Jeff King @ 2012-10-28 11:21 UTC (permalink / raw)
  To: Scott R. Godin; +Cc: git
In-Reply-To: <k63iai$kbp$1@ger.gmane.org>

On Mon, Oct 22, 2012 at 09:39:31AM -0400, Scott R. Godin wrote:

> As you can see from the below, I can't seem to get it to give me more
> verbose results of what's being merged (as in the actual merge below)
> with --stat or -v .. is it supposed to do that?

Yes. The diffstat is shown for the completed merge, but here:

> (develop)>$ git merge --no-commit --stat -v widget_twitter
> Automatic merge went well; stopped before committing as requested

You do not complete the merge.

> (develop|MERGING)>$ git merge --abort
> 
> (develop)>$ git merge widget_twitter
> Merge made by the 'recursive' strategy.
>  .../code/community/Dnd/Magentweet/Model/User.php   |    3 ++-
>  1 files changed, 2 insertions(+), 1 deletions(-)

Whereas here you do, and you get a diffstat.

When you are in the middle of an uncompleted merge and want to know what
is happening, you should look at the index using "git status" (to get an
overview of what is ready to be committed and what is unmerged), "git
diff --cached" (to see what was automatically merged and is ready for
commit), and "git diff" (to see conflicted entries that still need to be
resolved).

-Peff

^ permalink raw reply

* Re: [PATCH] Fix t9200 on case insensitive file systems
From: Jeff King @ 2012-10-28 11:10 UTC (permalink / raw)
  To: Torsten Bögershausen; +Cc: git, bdwalton, bosch, brian, robin.rosenberg
In-Reply-To: <201210261818.25620.tboegi@web.de>

On Fri, Oct 26, 2012 at 06:18:24PM +0200, Torsten Bögershausen wrote:

> t9200 defines $CVSROOT where cvs should init its repository
> $CVSROOT is set to $PWD/cvsroot.
> cvs init is supposed to create the repository inside $PWD/cvsroot/CVSROOT
> 
> "cvs init" (e.g. version  1.11.23) checks if the last element of the path is
> "CVSROOT", and if a directory with e.g. $PWD/cvsroot/CVSROOT already exists.
> 
> For such a $CVSROOT cvs refuses to init a repository here:
> "Cannot initialize repository under existing CVSROOT:
> 
> On a case insenstive file system cvsroot and CVSROOT are the same directories
> and t9200 fails.
> 
> Solution: use $PWD/tmp/cvsroot instead of cvsroot $PWD/cvsroot

Wouldn't tmp/cvsroot have the same problem, since the basename is still
cvsroot?

> diff --git a/t/t9200-git-cvsexportcommit.sh b/t/t9200-git-cvsexportcommit.sh
> index b59be9a..69934b2 100755
> --- a/t/t9200-git-cvsexportcommit.sh
> +++ b/t/t9200-git-cvsexportcommit.sh
> @@ -19,7 +19,7 @@ then
>      test_done
>  fi
>  
> -CVSROOT=$PWD/cvsroot
> +CVSROOT=$PWD/tmpcvsroot

Ah, but here you do something different, which makes sense. Should I
tweak the commit message?

-Peff

^ permalink raw reply

* Re: Mistake in git-reset documentation
From: Jeff King @ 2012-10-28 11:07 UTC (permalink / raw)
  To: Krzysztof Mazur; +Cc: Bojan Petrović, git
In-Reply-To: <20121028083610.GA26374@shrek.podlesie.net>

On Sun, Oct 28, 2012 at 09:36:10AM +0100, Krzysztof Mazur wrote:

> -- >8 --
> Subject: [PATCH] doc: git-reset: make "--<mode>" optional
> 
> The git-reset's "--<mode>" is an optional argument, however it was
> documented as required.
> 
> Signed-off-by: Krzysztof Mazur <krzysiek@podlesie.net>

I think this is sane, but...

>  DESCRIPTION
>  -----------
> @@ -43,7 +43,7 @@ This means that `git reset -p` is the opposite of `git add -p`, i.e.
>  you can use it to selectively reset hunks. See the ``Interactive Mode''
>  section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
>  
> -'git reset' --<mode> [<commit>]::
> +'git reset' [--<mode>] [<commit>]::
>  	This form resets the current branch head to <commit> and
>  	possibly updates the index (resetting it to the tree of <commit>) and
>  	the working tree depending on <mode>, which

Should we say something like "if --<mode> is omitted, defaults to
"--mixed"?

-Peff

^ permalink raw reply

* Re: [PATCH] mailmap: avoid out-of-bounds memory access
From: Jeff King @ 2012-10-28 11:02 UTC (permalink / raw)
  To: Romain Francoise; +Cc: git, gitster
In-Reply-To: <87k3ub4jjg.fsf@silenus.orebokech.com>

On Sun, Oct 28, 2012 at 12:49:55AM +0200, Romain Francoise wrote:

> AddressSanitizer (http://clang.llvm.org/docs/AddressSanitizer.html)
> complains of a one-byte buffer underflow in parse_name_and_email() while
> running the test suite. And indeed, if one of the lines in the mailmap
> begins with '<', we dereference the address just before the beginning of
> the buffer when looking for whitespace to remove, before checking that
> we aren't going too far.
> 
> So reverse the order of the tests to make sure that we don't read
> outside the buffer.

Thanks, I think your fix is correct.

> diff --git a/mailmap.c b/mailmap.c
> index 47aa419..ea4b471 100644
> --- a/mailmap.c
> +++ b/mailmap.c
> @@ -118,7 +118,7 @@ static char *parse_name_and_email(char *buffer, char **name,
>  	while (isspace(*nstart) && nstart < left)
>  		++nstart;
>  	nend = left-1;
> -	while (isspace(*nend) && nend > nstart)
> +	while (nend > nstart && isspace(*nend))
>  		--nend;

The fix confused me for a moment, because the problem is not actually in
the loop condition itself; working backwards from "nend > nstart", we
will at worst dereference nstart unnecessarily. The real problem is in
the "nend = left-1" above, which sets the loop precondition outside the
string to be examined.

So you could also check for "left == nstart" before the loop even
begins. I think your fix (to just make the loop more robust to that
precondition) is better, though, as the rest of the code does the right
thing with such a value of nend.

It looks like t4203 triggers this problem. Curious that valgrind does
not find it. I guess since it does not have compiler support, it cannot
find out-of-bound errors on stack buffers. Does the rest of the test
suite turn up clean with AddressSanitizer?

-Peff

^ permalink raw reply

* Re: Mistake in git-reset documentation
From: Bojan Petrović @ 2012-10-28  9:32 UTC (permalink / raw)
  To: Krzysztof Mazur; +Cc: git
In-Reply-To: <20121028083610.GA26374@shrek.podlesie.net>

No need for that. Getting this patched will be enough. :) Thank you!

On 28 October 2012 09:36, Krzysztof Mazur <krzysiek@podlesie.net> wrote:
> On Sat, Oct 27, 2012 at 01:21:18PM +0200, Bojan Petrović wrote:
>> None of the three forms of git-reset accept: "git reset" which is the
>> equivalent of "git reset -mixed".
>>
>> Square brackets should be used instead of parentheses for "--soft |
>> --mixed | --hard | --merge | --keep".
>>
>> Bojan
>
> Square brackets are also missing in "'git reset' --<mode> [<commit>]".
>
> Bojan, do you want to add a Reported-by line with your name?
>
> Krzysiek
>
> -- >8 --
> Subject: [PATCH] doc: git-reset: make "--<mode>" optional
>
> The git-reset's "--<mode>" is an optional argument, however it was
> documented as required.
>
> Signed-off-by: Krzysztof Mazur <krzysiek@podlesie.net>
> ---
>  Documentation/git-reset.txt | 4 ++--
>  1 file changed, 2 insertions(+), 2 deletions(-)
>
> diff --git a/Documentation/git-reset.txt b/Documentation/git-reset.txt
> index 117e374..1f95292 100644
> --- a/Documentation/git-reset.txt
> +++ b/Documentation/git-reset.txt
> @@ -10,7 +10,7 @@ SYNOPSIS
>  [verse]
>  'git reset' [-q] [<commit>] [--] <paths>...
>  'git reset' (--patch | -p) [<commit>] [--] [<paths>...]
> -'git reset' (--soft | --mixed | --hard | --merge | --keep) [-q] [<commit>]
> +'git reset' [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]
>
>  DESCRIPTION
>  -----------
> @@ -43,7 +43,7 @@ This means that `git reset -p` is the opposite of `git add -p`, i.e.
>  you can use it to selectively reset hunks. See the ``Interactive Mode''
>  section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
>
> -'git reset' --<mode> [<commit>]::
> +'git reset' [--<mode>] [<commit>]::
>         This form resets the current branch head to <commit> and
>         possibly updates the index (resetting it to the tree of <commit>) and
>         the working tree depending on <mode>, which
> --
> 1.8.0.46.gd11dae0
>

^ permalink raw reply

* Re: Mistake in git-reset documentation
From: Krzysztof Mazur @ 2012-10-28  8:36 UTC (permalink / raw)
  To: Bojan Petrović; +Cc: git
In-Reply-To: <CABPGWqr7=Rq4qS7yP09t2vMBUJ98NFTSmHUUgMzUQ5=WVrjfqg@mail.gmail.com>

On Sat, Oct 27, 2012 at 01:21:18PM +0200, Bojan Petrović wrote:
> None of the three forms of git-reset accept: "git reset" which is the
> equivalent of "git reset -mixed".
> 
> Square brackets should be used instead of parentheses for "--soft |
> --mixed | --hard | --merge | --keep".
> 
> Bojan

Square brackets are also missing in "'git reset' --<mode> [<commit>]".

Bojan, do you want to add a Reported-by line with your name?

Krzysiek

-- >8 --
Subject: [PATCH] doc: git-reset: make "--<mode>" optional

The git-reset's "--<mode>" is an optional argument, however it was
documented as required.

Signed-off-by: Krzysztof Mazur <krzysiek@podlesie.net>
---
 Documentation/git-reset.txt | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Documentation/git-reset.txt b/Documentation/git-reset.txt
index 117e374..1f95292 100644
--- a/Documentation/git-reset.txt
+++ b/Documentation/git-reset.txt
@@ -10,7 +10,7 @@ SYNOPSIS
 [verse]
 'git reset' [-q] [<commit>] [--] <paths>...
 'git reset' (--patch | -p) [<commit>] [--] [<paths>...]
-'git reset' (--soft | --mixed | --hard | --merge | --keep) [-q] [<commit>]
+'git reset' [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]
 
 DESCRIPTION
 -----------
@@ -43,7 +43,7 @@ This means that `git reset -p` is the opposite of `git add -p`, i.e.
 you can use it to selectively reset hunks. See the ``Interactive Mode''
 section of linkgit:git-add[1] to learn how to operate the `--patch` mode.
 
-'git reset' --<mode> [<commit>]::
+'git reset' [--<mode>] [<commit>]::
 	This form resets the current branch head to <commit> and
 	possibly updates the index (resetting it to the tree of <commit>) and
 	the working tree depending on <mode>, which
-- 
1.8.0.46.gd11dae0

^ permalink raw reply related

* Re: Can't understand the behaviour of git-diff --submodule
From: Francis Moreau @ 2012-10-28  8:26 UTC (permalink / raw)
  To: Jens Lehmann; +Cc: git
In-Reply-To: <508C6823.5060800@web.de>

On Sun, Oct 28, 2012 at 1:02 AM, Jens Lehmann <Jens.Lehmann@web.de> wrote:
> Am 26.10.2012 22:43, schrieb Francis Moreau:
>> On Fri, Oct 26, 2012 at 10:05 PM, Jens Lehmann <Jens.Lehmann@web.de> wrote:
>> [...]
>>>
>>> That is weird, "git diff --submodule" should show that too. Is there
>>> anything unusual about your setup? (The only explanation I can come
>>> up with after checking the code is that your submodule has neither a
>>> .git directory nor a gitfile or the objects directory in there doesn't
>>> contain these commits)
>>
>> Oh now you're asking, I think the submodule has been added by using
>> the --reference option of git-submodule-add.
>>
>>   $ cd configs
>>   $ cat .git
>>   gitdir: ../.git/modules/configs
>
> Thanks, I suspect the --reference option makes the difference here,
> I'll check that as soon as I find some time.

Yes that's fairly easy to reproduce:

$ mkdir super
$ cd super
$ git init
$ git submodule add --reference ~/tmp/git-submodule/public/a.git
~/tmp/git-submodule/public/a.git a
$ git commit -m "Initial creation of super project"
$ cd a
$ date >a.txt
$ git commit -a -m "add to a.txt a random change"
$ cd ..
$ git submodule summary
* a 2f8803a...a1aa4bf (1):
  > add to a.txt a random change
$ git commit -a -m "Include a's changes in super project"
$ git diff --submodule=log HEAD~1 HEAD
Submodule a 2f8803a...a1aa4bf (commits not present)

Thanks
-- 
Francis

^ permalink raw reply

* Re: [PATCH v2] Add new remote-hg transport helper
From: Felipe Contreras @ 2012-10-28  4:24 UTC (permalink / raw)
  To: Mike Hommey
  Cc: git, Junio C Hamano, Sverre Rabbelier, Ilari Liusvaara,
	Daniel Barkalow, Jeff King, Michael J Gruber
In-Reply-To: <20121020154301.GA31829@glandium.org>

On Sat, Oct 20, 2012 at 5:43 PM, Mike Hommey <mh@glandium.org> wrote:
> On Sat, Oct 20, 2012 at 05:00:06PM +0200, Felipe Contreras wrote:
>> Changes since v1:
>>
>>  * Improved documentation
>>  * Use more common 'python' binary
>>  * Warn, don't barf when a branch has multiple heads
>>  * Fixed marks to fetch after cloned
>>  * Support for cloning/pulling remote repositories
>>  * Use a more appropriate internal directory (e.g. .git/hg/origin)
>>  * Fixes for python3
>
> Are the resulting commits identical to what you'd get from using hg-git?

Now they are :)

-- 
Felipe Contreras

^ permalink raw reply

* [PATCH v4 13/13] remote-hg: add extra author test
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

For hg.hg.

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 t/t5802-remote-hg-hg-git.sh | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/t/t5802-remote-hg-hg-git.sh b/t/t5802-remote-hg-hg-git.sh
index 3cfa9e6..1f9f85c 100755
--- a/t/t5802-remote-hg-hg-git.sh
+++ b/t/t5802-remote-hg-hg-git.sh
@@ -353,7 +353,11 @@ test_expect_success 'hg author' '
 
 		echo theta > theta &&
 		hg add theta &&
-		hg commit -u "test >test@example.com>" -m "add theta"
+		hg commit -u "test >test@example.com>" -m "add theta" &&
+
+		echo iota > iota &&
+		hg add iota &&
+		hg commit -u "test <test <at> example <dot> com>" -m "add iota"
 		) &&
 
 		hg_push_$x hgrepo-$x gitrepo-$x &&
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 12/13] remote-hg: add tests to compare with hg-git
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

The base commands come from the tests of the hg-git project.

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 t/t5802-remote-hg-hg-git.sh | 445 ++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 445 insertions(+)
 create mode 100755 t/t5802-remote-hg-hg-git.sh

diff --git a/t/t5802-remote-hg-hg-git.sh b/t/t5802-remote-hg-hg-git.sh
new file mode 100755
index 0000000..3cfa9e6
--- /dev/null
+++ b/t/t5802-remote-hg-hg-git.sh
@@ -0,0 +1,445 @@
+#!/bin/sh
+#
+# Copyright (c) 2012 Felipe Contreras
+#
+# Base commands from hg-git tests:
+# https://bitbucket.org/durin42/hg-git/src
+#
+
+test_description='Test remote-hg output compared to hg-git'
+
+. ./test-lib.sh
+
+# clone to a git repo with git
+git_clone_git () {
+	hg -R $1 bookmark -f -r tip master &&
+	git clone -q "hg::$PWD/$1" $2
+}
+
+# clone to an hg repo with git
+hg_clone_git () {
+	(
+	hg init $2 &&
+	cd $1 &&
+	git push -q "hg::$PWD/../$2" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*'
+	) &&
+
+	(cd $2 && hg -q update)
+}
+
+# clone to a git repo with hg
+git_clone_hg () {
+	(
+	git init -q $2 &&
+	cd $1 &&
+	hg bookmark -f -r tip master &&
+	hg -q push -r master ../$2 || true
+	)
+}
+
+# clone to an hg repo with hg
+hg_clone_hg () {
+	hg -q clone $1 $2
+}
+
+# push an hg repo with git
+hg_push_git () {
+	(
+	cd $2
+	old=$(git symbolic-ref --short HEAD)
+	git checkout -q -b tmp &&
+	git fetch -q "hg::$PWD/../$1" 'refs/tags/*:refs/tags/*' 'refs/heads/*:refs/heads/*' &&
+	git checkout -q $old &&
+	git branch -q -D tmp 2> /dev/null || true
+	)
+}
+
+# push an hg git repo with hg
+hg_push_hg () {
+	(
+	cd $1 &&
+	hg -q push ../$2 || true
+	)
+}
+
+hg_log () {
+	hg -R $1 log --graph --debug | grep -v 'tag: *default/'
+}
+
+git_log () {
+	git --git-dir=$1/.git fast-export --branches
+}
+
+test_expect_success 'setup' '
+	(
+	echo "[ui]"
+	echo "username = A U Thor <author@example.com>"
+	echo "[defaults]"
+	echo "backout = -d \"0 0\""
+	echo "commit = -d \"0 0\""
+	echo "debugrawcommit = -d \"0 0\""
+	echo "tag = -d \"0 0\""
+	echo "[extensions]"
+	echo "hgext.bookmarks ="
+	echo "hggit ="
+	) >> "$HOME"/.hgrc &&
+	git config --global receive.denycurrentbranch warn
+	git config --global remote-hg.hg-git-compat true
+
+	export HGEDITOR=/usr/bin/true
+
+	export GIT_AUTHOR_DATE="2007-01-01 00:00:00 +0230"
+	export GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"
+'
+
+test_expect_success 'merge conflict 1' '
+	mkdir -p tmp && cd tmp &&
+	test_when_finished "cd .. && rm -rf tmp" &&
+
+	(
+	hg init hgrepo1 &&
+	cd hgrepo1 &&
+	echo A > afile &&
+	hg add afile &&
+	hg ci -m "origin" &&
+
+	echo B > afile &&
+	hg ci -m "A->B" &&
+
+	hg up -r0 &&
+	echo C > afile &&
+	hg ci -m "A->C" &&
+
+	hg merge -r1 || true &&
+	echo C > afile &&
+	hg resolve -m afile &&
+	hg ci -m "merge to C"
+	) &&
+
+	for x in hg git; do
+		git_clone_$x hgrepo1 gitrepo-$x &&
+		hg_clone_$x gitrepo-$x hgrepo2-$x &&
+		hg_log hgrepo2-$x > hg-log-$x &&
+		git_log gitrepo-$x > git-log-$x
+	done &&
+
+	test_cmp hg-log-hg hg-log-git &&
+	test_cmp git-log-hg git-log-git
+'
+
+test_expect_success 'merge conflict 2' '
+	mkdir -p tmp && cd tmp &&
+	test_when_finished "cd .. && rm -rf tmp" &&
+
+	(
+	hg init hgrepo1 &&
+	cd hgrepo1 &&
+	echo A > afile &&
+	hg add afile &&
+	hg ci -m "origin" &&
+
+	echo B > afile &&
+	hg ci -m "A->B" &&
+
+	hg up -r0 &&
+	echo C > afile &&
+	hg ci -m "A->C" &&
+
+	hg merge -r1 || true &&
+	echo B > afile &&
+	hg resolve -m afile &&
+	hg ci -m "merge to B"
+	) &&
+
+	for x in hg git; do
+		git_clone_$x hgrepo1 gitrepo-$x &&
+		hg_clone_$x gitrepo-$x hgrepo2-$x &&
+		hg_log hgrepo2-$x > hg-log-$x &&
+		git_log gitrepo-$x > git-log-$x
+	done &&
+
+	test_cmp hg-log-hg hg-log-git &&
+	test_cmp git-log-hg git-log-git
+'
+
+test_expect_success 'converged merge' '
+	mkdir -p tmp && cd tmp &&
+	test_when_finished "cd .. && rm -rf tmp" &&
+
+	(
+	hg init hgrepo1 &&
+	cd hgrepo1 &&
+	echo A > afile &&
+	hg add afile &&
+	hg ci -m "origin" &&
+
+	echo B > afile &&
+	hg ci -m "A->B" &&
+
+	echo C > afile &&
+	hg ci -m "B->C" &&
+
+	hg up -r0 &&
+	echo C > afile &&
+	hg ci -m "A->C" &&
+
+	hg merge -r2 || true &&
+	hg ci -m "merge"
+	) &&
+
+	for x in hg git; do
+		git_clone_$x hgrepo1 gitrepo-$x &&
+		hg_clone_$x gitrepo-$x hgrepo2-$x &&
+		hg_log hgrepo2-$x > hg-log-$x &&
+		git_log gitrepo-$x > git-log-$x
+	done &&
+
+	test_cmp hg-log-hg hg-log-git &&
+	test_cmp git-log-hg git-log-git
+'
+
+test_expect_success 'encoding' '
+	mkdir -p tmp && cd tmp &&
+	test_when_finished "cd .. && rm -rf tmp" &&
+
+	(
+	git init -q gitrepo &&
+	cd gitrepo &&
+
+	echo alpha > alpha &&
+	git add alpha &&
+	git commit -m "add älphà" &&
+
+	export GIT_AUTHOR_NAME="tést èncödîng" &&
+	echo beta > beta &&
+	git add beta &&
+	git commit -m "add beta" &&
+
+	echo gamma > gamma &&
+	git add gamma &&
+	git commit -m "add gämmâ" &&
+
+	: TODO git config i18n.commitencoding latin-1 &&
+	echo delta > delta &&
+	git add delta &&
+	git commit -m "add déltà"
+	) &&
+
+	for x in hg git; do
+		hg_clone_$x gitrepo hgrepo-$x &&
+		git_clone_$x hgrepo-$x gitrepo2-$x &&
+
+		HGENCODING=utf-8 hg_log hgrepo-$x > hg-log-$x &&
+		git_log gitrepo2-$x > git-log-$x
+	done &&
+
+	test_cmp hg-log-hg hg-log-git &&
+	test_cmp git-log-hg git-log-git
+'
+
+test_expect_success 'file removal' '
+	mkdir -p tmp && cd tmp &&
+	test_when_finished "cd .. && rm -rf tmp" &&
+
+	(
+	git init -q gitrepo &&
+	cd gitrepo &&
+	echo alpha > alpha &&
+	git add alpha &&
+	git commit -m "add alpha" &&
+	echo beta > beta &&
+	git add beta &&
+	git commit -m "add beta"
+	mkdir foo &&
+	echo blah > foo/bar &&
+	git add foo &&
+	git commit -m "add foo" &&
+	git rm alpha &&
+	git commit -m "remove alpha" &&
+	git rm foo/bar &&
+	git commit -m "remove foo/bar"
+	) &&
+
+	for x in hg git; do
+		(
+		hg_clone_$x gitrepo hgrepo-$x &&
+		cd hgrepo-$x &&
+		hg_log . &&
+		hg manifest -r 3 &&
+		hg manifest
+		) > output-$x &&
+
+		git_clone_$x hgrepo-$x gitrepo2-$x &&
+		git_log gitrepo2-$x > log-$x
+	done &&
+
+	test_cmp output-hg output-git &&
+	test_cmp log-hg log-git
+'
+
+test_expect_success 'git tags' '
+	mkdir -p tmp && cd tmp &&
+	test_when_finished "cd .. && rm -rf tmp" &&
+
+	(
+	git init -q gitrepo &&
+	cd gitrepo &&
+	git config receive.denyCurrentBranch ignore &&
+	echo alpha > alpha &&
+	git add alpha &&
+	git commit -m "add alpha" &&
+	git tag alpha &&
+
+	echo beta > beta &&
+	git add beta &&
+	git commit -m "add beta" &&
+	git tag -a -m "added tag beta" beta
+	) &&
+
+	for x in hg git; do
+		hg_clone_$x gitrepo hgrepo-$x &&
+		hg_log hgrepo-$x > log-$x
+	done &&
+
+	test_cmp log-hg log-git
+'
+
+test_expect_success 'hg author' '
+	mkdir -p tmp && cd tmp &&
+	test_when_finished "cd .. && rm -rf tmp" &&
+
+	for x in hg git; do
+		(
+		git init -q gitrepo-$x &&
+		cd gitrepo-$x &&
+
+		echo alpha > alpha &&
+		git add alpha &&
+		git commit -m "add alpha" &&
+		git checkout -q -b not-master
+		) &&
+
+		(
+		hg_clone_$x gitrepo-$x hgrepo-$x &&
+		cd hgrepo-$x &&
+
+		hg co master &&
+		echo beta > beta &&
+		hg add beta &&
+		hg commit -u "test" -m "add beta" &&
+
+		echo gamma >> beta &&
+		hg commit -u "test <test@example.com> (comment)" -m "modify beta" &&
+
+		echo gamma > gamma &&
+		hg add gamma &&
+		hg commit -u "<test@example.com>" -m "add gamma" &&
+
+		echo delta > delta &&
+		hg add delta &&
+		hg commit -u "name<test@example.com>" -m "add delta" &&
+
+		echo epsilon > epsilon &&
+		hg add epsilon &&
+		hg commit -u "name <test@example.com" -m "add epsilon" &&
+
+		echo zeta > zeta &&
+		hg add zeta &&
+		hg commit -u " test " -m "add zeta" &&
+
+		echo eta > eta &&
+		hg add eta &&
+		hg commit -u "test < test@example.com >" -m "add eta" &&
+
+		echo theta > theta &&
+		hg add theta &&
+		hg commit -u "test >test@example.com>" -m "add theta"
+		) &&
+
+		hg_push_$x hgrepo-$x gitrepo-$x &&
+		hg_clone_$x gitrepo-$x hgrepo2-$x &&
+
+		hg_log hgrepo2-$x > hg-log-$x &&
+		git_log gitrepo-$x > git-log-$x
+	done &&
+
+	test_cmp git-log-hg git-log-git &&
+
+	test_cmp hg-log-hg hg-log-git &&
+	test_cmp git-log-hg git-log-git
+'
+
+test_expect_success 'hg branch' '
+	mkdir -p tmp && cd tmp &&
+	test_when_finished "cd .. && rm -rf tmp" &&
+
+	for x in hg git; do
+		(
+		git init -q gitrepo-$x &&
+		cd gitrepo-$x &&
+
+		echo alpha > alpha &&
+		git add alpha &&
+		git commit -q -m "add alpha" &&
+		git checkout -q -b not-master
+		) &&
+
+		(
+		hg_clone_$x gitrepo-$x hgrepo-$x &&
+
+		cd hgrepo-$x &&
+		hg -q co master &&
+		hg mv alpha beta &&
+		hg -q commit -m "rename alpha to beta" &&
+		hg branch gamma | grep -v "permanent and global" &&
+		hg -q commit -m "started branch gamma"
+		) &&
+
+		hg_push_$x hgrepo-$x gitrepo-$x &&
+		hg_clone_$x gitrepo-$x hgrepo2-$x &&
+
+		hg_log hgrepo2-$x > hg-log-$x &&
+		git_log gitrepo-$x > git-log-$x
+	done &&
+
+	test_cmp hg-log-hg hg-log-git &&
+	test_cmp git-log-hg git-log-git
+'
+
+test_expect_success 'hg tags' '
+	mkdir -p tmp && cd tmp &&
+	test_when_finished "cd .. && rm -rf tmp" &&
+
+	for x in hg git; do
+		(
+		git init -q gitrepo-$x &&
+		cd gitrepo-$x &&
+
+		echo alpha > alpha &&
+		git add alpha &&
+		git commit -m "add alpha" &&
+		git checkout -q -b not-master
+		) &&
+
+		(
+		hg_clone_$x gitrepo-$x hgrepo-$x &&
+
+		cd hgrepo-$x &&
+		hg co master &&
+		hg tag alpha
+		) &&
+
+		hg_push_$x hgrepo-$x gitrepo-$x &&
+		hg_clone_$x gitrepo-$x hgrepo2-$x &&
+
+		(
+		git --git-dir=gitrepo-hg/.git tag -l &&
+		hg_log hgrepo2-hg &&
+		cat hgrepo2-hg/.hgtags
+		) > output-$x
+	done &&
+
+	test_cmp output-hg output-git
+'
+
+test_done
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 11/13] remote-hg: add support for fake remote
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Helpful while testing.

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
index e8e3791..092020f 100755
--- a/contrib/remote-hg/git-remote-hg
+++ b/contrib/remote-hg/git-remote-hg
@@ -245,7 +245,13 @@ def get_repo(url, alias):
     myui = ui.ui()
     myui.setconfig('ui', 'interactive', 'off')
 
-    if hg.islocal(url):
+    if url.startswith("remote://"):
+        remote = True
+        url = "file://%s" % url[9:]
+    else:
+        remote = False
+
+    if hg.islocal(url) and not remote:
         repo = hg.repository(myui, url)
     else:
         local_path = os.path.join(dirname, 'clone')
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 10/13] remote-hg: fake bookmark when there's none
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Or at least no current bookmark.

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 16 ++++++++++++----
 1 file changed, 12 insertions(+), 4 deletions(-)

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
index 3bb3192..e8e3791 100755
--- a/contrib/remote-hg/git-remote-hg
+++ b/contrib/remote-hg/git-remote-hg
@@ -419,12 +419,20 @@ def list_branch_head(repo, cur):
     g_head = (head, 'branches', repo[tip])
 
 def list_bookmark_head(repo, cur):
-    global g_head
+    global g_head, bmarks
 
     head = bookmarks.readcurrent(repo)
-    if not head:
-        return
-    node = repo[head]
+    if head:
+        node = repo[head]
+    else:
+        # fake bookmark from current branch
+        head = cur
+        tip = get_branch_tip(repo, head)
+        if not tip:
+            return
+        node = repo[tip]
+        bmarks[head] = node
+
     print "@refs/heads/%s HEAD" % head
     g_head = (head, 'bookmarks', node)
 
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 08/13] remote-hg: add support for hg-git compat mode
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 102 +++++++++++++++++++++++++++++++++++++---
 1 file changed, 95 insertions(+), 7 deletions(-)

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
index 57e54c2..47bb7c1 100755
--- a/contrib/remote-hg/git-remote-hg
+++ b/contrib/remote-hg/git-remote-hg
@@ -13,6 +13,22 @@ import sys
 import os
 import json
 import shutil
+import subprocess
+
+#
+# If you want to switch to hg-git compatibility mode:
+# git config --global remote-hg.hg-git-compat true
+#
+# git:
+# Sensible defaults for git.
+# hg bookmarks are exported as git branches, hg branches are prefixed
+# with 'branches/'.
+#
+# hg:
+# Emulate hg-git.
+# Only hg bookmarks are exported as git branches.
+# Commits are modified to preserve hg information and allow biridectionality.
+#
 
 NAME_RE = re.compile('^([^<>]+)')
 AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$')
@@ -209,7 +225,7 @@ def mark_to_rev(mark):
     return marks.to_rev(mark)
 
 def export_ref(repo, name, kind, head):
-    global prefix, marks
+    global prefix, marks, mode
 
     ename = '%s/%s' % (kind, name)
     tip = marks.get_tip(ename)
@@ -244,6 +260,33 @@ def export_ref(repo, name, kind, head):
         else:
             modified, removed = get_filechanges(repo, c, parents)
 
+        if mode == 'hg':
+            extra_msg = ''
+
+            if rev_branch != 'default':
+                extra_msg += 'branch : %s\n' % rev_branch
+
+            renames = []
+            for f in c.files():
+                if f not in c.manifest():
+                    continue
+                rename = c.filectx(f).renamed()
+                if rename:
+                    renames.append((rename[0], f))
+
+            for e in renames:
+                extra_msg += "rename : %s => %s\n" % e
+
+            for key, value in extra.iteritems():
+                if key in ('author', 'committer', 'encoding', 'message', 'branch', 'hg-git'):
+                    continue
+                else:
+                    extra_msg += "extra : %s : %s\n" % (key, urllib.quote(value))
+
+            desc += '\n'
+            if extra_msg:
+                desc += '\n--HG--\n' + extra_msg
+
         if len(parents) == 0 and rev:
             print 'reset %s/%s' % (prefix, ename)
 
@@ -332,8 +375,18 @@ def list_branch_head(repo, cur):
     print "@refs/heads/%s HEAD" % head
     g_head = (head, 'branches', repo[tip])
 
+def list_bookmark_head(repo, cur):
+    global g_head
+
+    head = bookmarks.readcurrent(repo)
+    if not head:
+        return
+    node = repo[head]
+    print "@refs/heads/%s HEAD" % head
+    g_head = (head, 'bookmarks', node)
+
 def do_list(parser):
-    global branches, bmarks
+    global branches, bmarks, mode
 
     repo = parser.repo
     for branch in repo.branchmap():
@@ -346,9 +399,13 @@ def do_list(parser):
 
     cur = repo.dirstate.branch()
 
-    list_branch_head(repo, cur)
-    for branch in branches:
-        print "? refs/heads/branches/%s" % branch
+    if mode != 'hg':
+        list_branch_head(repo, cur)
+        for branch in branches:
+            print "? refs/heads/branches/%s" % branch
+    else:
+        list_bookmark_head(repo, cur)
+
     for bmark in bmarks:
         print "? refs/heads/%s" % bmark
 
@@ -414,6 +471,7 @@ def get_merge_files(repo, p1, p2, files):
 
 def parse_commit(parser):
     global marks, blob_marks, bmarks, parsed_refs
+    global mode
 
     from_mark = merge_mark = None
 
@@ -460,7 +518,9 @@ def parse_commit(parser):
             return of['ctx']
         is_exec = of['mode'] == 'x'
         is_link = of['mode'] == 'l'
-        return context.memfilectx(f, of['data'], is_link, is_exec, None)
+        rename = of.get('rename', None)
+        return context.memfilectx(f, of['data'],
+                is_link, is_exec, rename)
 
     repo = parser.repo
 
@@ -487,6 +547,21 @@ def parse_commit(parser):
     if merge_mark:
         get_merge_files(repo, p1, p2, files)
 
+    if mode == 'hg':
+        i = data.find('\n--HG--\n')
+        if i >= 0:
+            tmp = data[i + len('\n--HG--\n'):].strip()
+            for k, v in [e.split(' : ') for e in tmp.split('\n')]:
+                if k == 'rename':
+                    old, new = v.split(' => ', 1)
+                    files[new]['rename'] = old
+                elif k == 'branch':
+                    extra[k] = v
+                elif k == 'extra':
+                    ek, ev = v.split(' : ', 1)
+                    extra[ek] = urllib.unquote(ev)
+            data = data[:i]
+
     ctx = context.memctx(repo, (p1, p2), data,
             files.keys(), getfilectx,
             user, (date, tz), extra)
@@ -571,12 +646,25 @@ def do_export(parser):
 def main(args):
     global prefix, dirname, branches, bmarks
     global marks, blob_marks, parsed_refs
-    global peer
+    global peer, mode
 
     alias = args[1]
     url = args[2]
     peer = None
 
+    cmd = ['git', 'config', '--get', 'remote-hg.hg-git-compat']
+    hg_git_compat = False
+    try:
+        if subprocess.check_output(cmd) == 'true\n':
+            hg_git_compat = True
+    except subprocess.CalledProcessError:
+        pass
+
+    if hg_git_compat:
+        mode = 'hg'
+    else:
+        mode = 'git'
+
     if alias[4:] == url:
         is_tmp = True
         alias = util.sha1(alias).hexdigest()
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 09/13] remote-hg: add compat for hg-git author fixes
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 59 ++++++++++++++++++++++++++++++++++++-----
 1 file changed, 53 insertions(+), 6 deletions(-)

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
index 47bb7c1..3bb3192 100755
--- a/contrib/remote-hg/git-remote-hg
+++ b/contrib/remote-hg/git-remote-hg
@@ -14,6 +14,7 @@ import os
 import json
 import shutil
 import subprocess
+import urllib
 
 #
 # If you want to switch to hg-git compatibility mode:
@@ -32,6 +33,7 @@ import subprocess
 
 NAME_RE = re.compile('^([^<>]+)')
 AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$')
+AUTHOR_HG_RE = re.compile('^(.*?) ?<(.+?)(?:>(.+)?)?$')
 RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.+)> (\d+) ([+-]\d+)')
 
 def die(msg, *args):
@@ -149,12 +151,20 @@ class Parser:
         return sys.stdin.read(size)
 
     def get_author(self):
+        global bad_mail
+
+        ex = None
         m = RAW_AUTHOR_RE.match(self.line)
         if not m:
             return None
         _, name, email, date, tz = m.groups()
+        if name and 'ext:' in name:
+            m = re.match('^(.+?) ext:\((.+)\)$', name)
+            if m:
+                name = m.group(1)
+                ex = urllib.unquote(m.group(2))
 
-        if email != 'unknown':
+        if email != bad_mail:
             if name:
                 user = '%s <%s>' % (name, email)
             else:
@@ -162,6 +172,9 @@ class Parser:
         else:
             user = name
 
+        if ex:
+            user += ex
+
         tz = int(tz)
         tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
         return (user, int(date), -tz)
@@ -177,9 +190,9 @@ def get_filechanges(repo, ctx, parents):
     changed, added, removed = [set(sum(e, [])) for e in zip(*l)]
     return added | changed, removed
 
-def fixup_user(user):
-    user = user.replace('"', '')
+def fixup_user_git(user):
     name = mail = None
+    user = user.replace('"', '')
     m = AUTHOR_RE.match(user)
     if m:
         name = m.group(1)
@@ -188,11 +201,41 @@ def fixup_user(user):
         m = NAME_RE.match(user)
         if m:
             name = m.group(1).strip()
+    return (name, mail)
+
+def fixup_user_hg(user):
+    def sanitize(name):
+        # stole this from hg-git
+        return re.sub('[<>\n]', '?', name.lstrip('< ').rstrip('> '))
+
+    m = AUTHOR_HG_RE.match(user)
+    if m:
+        name = sanitize(m.group(1))
+        mail = sanitize(m.group(2))
+        ex = m.group(3)
+        if ex:
+            name += ' ext:(' + urllib.quote(ex) + ')'
+    else:
+        name = sanitize(user)
+        if '@' in user:
+            mail = name
+        else:
+            mail = None
+
+    return (name, mail)
+
+def fixup_user(user):
+    global mode, bad_mail
+
+    if mode == 'git':
+        name, mail = fixup_user_git(user)
+    else:
+        name, mail = fixup_user_hg(user)
 
     if not name:
-        name = 'Unknown'
+        name = bad_name
     if not mail:
-        mail = 'unknown'
+        mail = bad_mail
 
     return '%s <%s>' % (name, mail)
 
@@ -646,7 +689,7 @@ def do_export(parser):
 def main(args):
     global prefix, dirname, branches, bmarks
     global marks, blob_marks, parsed_refs
-    global peer, mode
+    global peer, mode, bad_mail, bad_name
 
     alias = args[1]
     url = args[2]
@@ -662,8 +705,12 @@ def main(args):
 
     if hg_git_compat:
         mode = 'hg'
+        bad_mail = 'none@none'
+        bad_name = ''
     else:
         mode = 'git'
+        bad_mail = 'unknown'
+        bad_name = 'Unknown'
 
     if alias[4:] == url:
         is_tmp = True
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 07/13] remote-hg: match hg merge behavior
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 15 +++++++++++++++
 1 file changed, 15 insertions(+)

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
index 1689573..57e54c2 100755
--- a/contrib/remote-hg/git-remote-hg
+++ b/contrib/remote-hg/git-remote-hg
@@ -406,6 +406,12 @@ def parse_blob(parser):
     parser.next()
     return
 
+def get_merge_files(repo, p1, p2, files):
+    for e in repo[p1].files():
+        if e not in files:
+            f = { 'ctx' : repo[p1][e] }
+            files[e] = f
+
 def parse_commit(parser):
     global marks, blob_marks, bmarks, parsed_refs
 
@@ -450,6 +456,8 @@ def parse_commit(parser):
         of = files[f]
         if 'deleted' in of:
             raise IOError
+        if 'ctx' in of:
+            return of['ctx']
         is_exec = of['mode'] == 'x'
         is_link = of['mode'] == 'l'
         return context.memfilectx(f, of['data'], is_link, is_exec, None)
@@ -472,6 +480,13 @@ def parse_commit(parser):
     else:
         p2 = '\0' * 20
 
+    #
+    # If files changed from any of the parents, hg wants to know, but in git if
+    # nothing changed from the first parent, nothing changed.
+    #
+    if merge_mark:
+        get_merge_files(repo, p1, p2, files)
+
     ctx = context.memctx(repo, (p1, p2), data,
             files.keys(), getfilectx,
             user, (date, tz), extra)
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 06/13] remote-hg: make sure the encoding is correct
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Independently of the environment.

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
index c96e1a8..1689573 100755
--- a/contrib/remote-hg/git-remote-hg
+++ b/contrib/remote-hg/git-remote-hg
@@ -6,7 +6,7 @@
 # Then you can clone with:
 # git clone hg::/path/to/mercurial/repo/
 
-from mercurial import hg, ui, bookmarks, context, util
+from mercurial import hg, ui, bookmarks, context, util, encoding
 
 import re
 import sys
@@ -370,6 +370,9 @@ def do_import(parser):
     print "feature export-marks=%s" % path
     sys.stdout.flush()
 
+    tmp = encoding.encoding
+    encoding.encoding = 'utf-8'
+
     # lets get all the import lines
     while parser.check('import'):
         ref = parser[1]
@@ -388,6 +391,8 @@ def do_import(parser):
 
         parser.next()
 
+    encoding.encoding = tmp
+
     print 'done'
 
 def parse_blob(parser):
@@ -471,8 +476,13 @@ def parse_commit(parser):
             files.keys(), getfilectx,
             user, (date, tz), extra)
 
+    tmp = encoding.encoding
+    encoding.encoding = 'utf-8'
+
     node = repo.commitctx(ctx)
 
+    encoding.encoding = tmp
+
     rev = repo[node].rev()
 
     parsed_refs[ref] = node
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 05/13] remote-hg: add support to push URLs
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
index b10e7d1..c96e1a8 100755
--- a/contrib/remote-hg/git-remote-hg
+++ b/contrib/remote-hg/git-remote-hg
@@ -6,12 +6,13 @@
 # Then you can clone with:
 # git clone hg::/path/to/mercurial/repo/
 
-from mercurial import hg, ui, bookmarks, context
+from mercurial import hg, ui, bookmarks, context, util
 
 import re
 import sys
 import os
 import json
+import shutil
 
 NAME_RE = re.compile('^([^<>]+)')
 AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$')
@@ -551,6 +552,12 @@ def main(args):
     url = args[2]
     peer = None
 
+    if alias[4:] == url:
+        is_tmp = True
+        alias = util.sha1(alias).hexdigest()
+    else:
+        is_tmp = False
+
     gitdir = os.environ['GIT_DIR']
     dirname = os.path.join(gitdir, 'hg', alias)
     branches = {}
@@ -581,6 +588,9 @@ def main(args):
             die('unhandled command: %s' % line)
         sys.stdout.flush()
 
-    marks.store()
+    if not is_tmp:
+        marks.store()
+    else:
+        shutil.rmtree(dirname)
 
 sys.exit(main(sys.argv))
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 04/13] remote-hg: add support for remote pushing
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
index 4021a7d..b10e7d1 100755
--- a/contrib/remote-hg/git-remote-hg
+++ b/contrib/remote-hg/git-remote-hg
@@ -180,7 +180,7 @@ def fixup_user(user):
     return '%s <%s>' % (name, mail)
 
 def get_repo(url, alias):
-    global dirname
+    global dirname, peer
 
     myui = ui.ui()
     myui.setconfig('ui', 'interactive', 'off')
@@ -508,7 +508,7 @@ def parse_tag(parser):
     # nothing to do
 
 def do_export(parser):
-    global parsed_refs
+    global parsed_refs, peer
 
     parser.next()
 
@@ -539,12 +539,17 @@ def do_export(parser):
 
     print
 
+    if peer:
+        parser.repo.push(peer, force=False)
+
 def main(args):
     global prefix, dirname, branches, bmarks
     global marks, blob_marks, parsed_refs
+    global peer
 
     alias = args[1]
     url = args[2]
+    peer = None
 
     gitdir = os.environ['GIT_DIR']
     dirname = os.path.join(gitdir, 'hg', alias)
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 03/13] remote-hg: add support for pushing
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 215 +++++++++++++++++++++++++++++++++++++++-
 1 file changed, 213 insertions(+), 2 deletions(-)

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
index 4ba9ee6..4021a7d 100755
--- a/contrib/remote-hg/git-remote-hg
+++ b/contrib/remote-hg/git-remote-hg
@@ -6,7 +6,7 @@
 # Then you can clone with:
 # git clone hg::/path/to/mercurial/repo/
 
-from mercurial import hg, ui, bookmarks
+from mercurial import hg, ui, bookmarks, context
 
 import re
 import sys
@@ -15,6 +15,7 @@ import json
 
 NAME_RE = re.compile('^([^<>]+)')
 AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$')
+RAW_AUTHOR_RE = re.compile('^(\w+) (?:(.+)? )?<(.+)> (\d+) ([+-]\d+)')
 
 def die(msg, *args):
     sys.stderr.write('ERROR: %s\n' % (msg % args))
@@ -29,12 +30,17 @@ def gitmode(flags):
 def gittz(tz):
     return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
 
+def hgmode(mode):
+    m = { '0100755': 'x', '0120000': 'l' }
+    return m.get(mode, '')
+
 class Marks:
 
     def __init__(self, path):
         self.path = path
         self.tips = {}
         self.marks = {}
+        self.rev_marks = {}
         self.last_mark = 0
 
         self.load()
@@ -49,6 +55,9 @@ class Marks:
         self.marks = tmp['marks']
         self.last_mark = tmp['last-mark']
 
+        for rev, mark in self.marks.iteritems():
+            self.rev_marks[mark] = int(rev)
+
     def dict(self):
         return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
 
@@ -61,11 +70,19 @@ class Marks:
     def from_rev(self, rev):
         return self.marks[str(rev)]
 
+    def to_rev(self, mark):
+        return self.rev_marks[mark]
+
     def next_mark(self, rev):
         self.last_mark += 1
         self.marks[str(rev)] = self.last_mark
         return self.last_mark
 
+    def new_mark(self, rev, mark):
+        self.marks[str(rev)] = mark
+        self.rev_marks[mark] = rev
+        self.last_mark = mark
+
     def is_marked(self, rev):
         return self.marks.has_key(str(rev))
 
@@ -103,6 +120,35 @@ class Parser:
         if self.line == 'done':
             self.line = None
 
+    def get_mark(self):
+        i = self.line.index(':') + 1
+        return int(self.line[i:])
+
+    def get_data(self):
+        if not self.check('data'):
+            return None
+        i = self.line.index(' ') + 1
+        size = int(self.line[i:])
+        return sys.stdin.read(size)
+
+    def get_author(self):
+        m = RAW_AUTHOR_RE.match(self.line)
+        if not m:
+            return None
+        _, name, email, date, tz = m.groups()
+
+        if email != 'unknown':
+            if name:
+                user = '%s <%s>' % (name, email)
+            else:
+                user = '<%s>' % (email)
+        else:
+            user = name
+
+        tz = int(tz)
+        tz = ((tz / 100) * 3600) + ((tz % 100) * 60)
+        return (user, int(date), -tz)
+
 def export_file(fc):
     d = fc.data()
     print "M %s inline %s" % (gitmode(fc.flags()), fc.path())
@@ -157,6 +203,10 @@ def rev_to_mark(rev):
     global marks
     return marks.from_rev(rev)
 
+def mark_to_rev(mark):
+    global marks
+    return marks.to_rev(mark)
+
 def export_ref(repo, name, kind, head):
     global prefix, marks
 
@@ -246,9 +296,17 @@ def do_capabilities(parser):
     global prefix, dirname
 
     print "import"
+    print "export"
     print "refspec refs/heads/branches/*:%s/branches/*" % prefix
     print "refspec refs/heads/*:%s/bookmarks/*" % prefix
     print "refspec refs/tags/*:%s/tags/*" % prefix
+
+    path = os.path.join(dirname, 'marks-git')
+
+    if os.path.exists(path):
+        print "*import-marks %s" % path
+    print "*export-marks %s" % path
+
     print
 
 def get_branch_tip(repo, branch):
@@ -331,8 +389,159 @@ def do_import(parser):
 
     print 'done'
 
+def parse_blob(parser):
+    global blob_marks
+
+    parser.next()
+    mark = parser.get_mark()
+    parser.next()
+    data = parser.get_data()
+    blob_marks[mark] = data
+    parser.next()
+    return
+
+def parse_commit(parser):
+    global marks, blob_marks, bmarks, parsed_refs
+
+    from_mark = merge_mark = None
+
+    a = parser.line.split(' ')
+    ref = a[1]
+    parser.next()
+
+    commit_mark = parser.get_mark()
+    parser.next()
+    author = parser.get_author()
+    parser.next()
+    committer = parser.get_author()
+    parser.next()
+    data = parser.get_data()
+    parser.next()
+    if parser.check('from'):
+        from_mark = parser.get_mark()
+        parser.next()
+    if parser.check('merge'):
+        merge_mark = parser.get_mark()
+        parser.next()
+        if parser.check('merge'):
+            die('octopus merges are not supported yet')
+
+    files = {}
+
+    for line in parser:
+        if parser.check('M'):
+            t, m, mark_ref, path = line.split(' ')
+            mark = int(mark_ref[1:])
+            f = { 'mode' : hgmode(m), 'data' : blob_marks[mark] }
+        elif parser.check('D'):
+            t, path = line.split(' ')
+            f = { 'deleted' : True }
+        else:
+            die('Unknown file command: %s' % line)
+        files[path] = f
+
+    def getfilectx(repo, memctx, f):
+        of = files[f]
+        if 'deleted' in of:
+            raise IOError
+        is_exec = of['mode'] == 'x'
+        is_link = of['mode'] == 'l'
+        return context.memfilectx(f, of['data'], is_link, is_exec, None)
+
+    repo = parser.repo
+
+    user, date, tz = author
+    extra = {}
+
+    if committer != author:
+        extra['committer'] = "%s %u %u" % committer
+
+    if from_mark:
+        p1 = repo.changelog.node(mark_to_rev(from_mark))
+    else:
+        p1 = '\0' * 20
+
+    if merge_mark:
+        p2 = repo.changelog.node(mark_to_rev(merge_mark))
+    else:
+        p2 = '\0' * 20
+
+    ctx = context.memctx(repo, (p1, p2), data,
+            files.keys(), getfilectx,
+            user, (date, tz), extra)
+
+    node = repo.commitctx(ctx)
+
+    rev = repo[node].rev()
+
+    parsed_refs[ref] = node
+
+    marks.new_mark(rev, commit_mark)
+
+def parse_reset(parser):
+    a = parser.line.split(' ')
+    ref = a[1]
+    parser.next()
+    # ugh
+    if parser.check('commit'):
+        parse_commit(parser)
+        return
+    if not parser.check('from'):
+        return
+    from_mark = parser.get_mark()
+    parser.next()
+
+    node = parser.repo.changelog.node(mark_to_rev(from_mark))
+    parsed_refs[ref] = node
+
+def parse_tag(parser):
+    a = parser.line.split(' ')
+    name = a[1]
+    parser.next()
+    from_mark = parser.get_mark()
+    parser.next()
+    tagger = parser.get_author()
+    parser.next()
+    data = parser.get_data()
+    parser.next()
+
+    # nothing to do
+
+def do_export(parser):
+    global parsed_refs
+
+    parser.next()
+
+    for line in parser.each_block('done'):
+        if parser.check('blob'):
+            parse_blob(parser)
+        elif parser.check('commit'):
+            parse_commit(parser)
+        elif parser.check('reset'):
+            parse_reset(parser)
+        elif parser.check('tag'):
+            parse_tag(parser)
+        elif parser.check('feature'):
+            pass
+        else:
+            die('unhandled export command: %s' % line)
+
+    for ref, node in parsed_refs.iteritems():
+        if ref.startswith('refs/heads/branches'):
+            pass
+        elif ref.startswith('refs/heads/'):
+            bmark = ref[len('refs/heads/'):]
+            bookmarks.pushbookmark(parser.repo, bmark, '', node)
+        elif ref.startswith('refs/tags/'):
+            tag = ref[len('refs/tags/'):]
+            parser.repo.tag([tag], node, None, True, None, {})
+        print "ok %s" % ref
+
+    print
+
 def main(args):
-    global prefix, dirname, marks, branches, bmarks
+    global prefix, dirname, branches, bmarks
+    global marks, blob_marks, parsed_refs
 
     alias = args[1]
     url = args[2]
@@ -341,6 +550,8 @@ def main(args):
     dirname = os.path.join(gitdir, 'hg', alias)
     branches = {}
     bmarks = {}
+    blob_marks = {}
+    parsed_refs = {}
 
     repo = get_repo(url, alias)
     prefix = 'refs/hg/%s' % alias
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 02/13] remote-hg: add support for bookmarks
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 20 +++++++++++++++++---
 1 file changed, 17 insertions(+), 3 deletions(-)

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
index c771182..4ba9ee6 100755
--- a/contrib/remote-hg/git-remote-hg
+++ b/contrib/remote-hg/git-remote-hg
@@ -6,7 +6,7 @@
 # Then you can clone with:
 # git clone hg::/path/to/mercurial/repo/
 
-from mercurial import hg, ui
+from mercurial import hg, ui, bookmarks
 
 import re
 import sys
@@ -229,6 +229,10 @@ def export_ref(repo, name, kind, head):
 def export_tag(repo, tag):
     export_ref(repo, tag, 'tags', repo[tag])
 
+def export_bookmark(repo, bmark):
+    head = bmarks[bmark]
+    export_ref(repo, bmark, 'bookmarks', head)
+
 def export_branch(repo, branch):
     tip = get_branch_tip(repo, branch)
     head = repo[tip]
@@ -243,6 +247,7 @@ def do_capabilities(parser):
 
     print "import"
     print "refspec refs/heads/branches/*:%s/branches/*" % prefix
+    print "refspec refs/heads/*:%s/bookmarks/*" % prefix
     print "refspec refs/tags/*:%s/tags/*" % prefix
     print
 
@@ -269,7 +274,7 @@ def list_branch_head(repo, cur):
     g_head = (head, 'branches', repo[tip])
 
 def do_list(parser):
-    global branches
+    global branches, bmarks
 
     repo = parser.repo
     for branch in repo.branchmap():
@@ -277,11 +282,16 @@ def do_list(parser):
         if len(heads):
             branches[branch] = heads
 
+    for bmark, node in bookmarks.listbookmarks(repo).iteritems():
+        bmarks[bmark] = repo[node]
+
     cur = repo.dirstate.branch()
 
     list_branch_head(repo, cur)
     for branch in branches:
         print "? refs/heads/branches/%s" % branch
+    for bmark in bmarks:
+        print "? refs/heads/%s" % bmark
 
     for tag, node in repo.tagslist():
         if tag == 'tip':
@@ -310,6 +320,9 @@ def do_import(parser):
         elif ref.startswith('refs/heads/branches/'):
             branch = ref[len('refs/heads/branches/'):]
             export_branch(repo, branch)
+        elif ref.startswith('refs/heads/'):
+            bmark = ref[len('refs/heads/'):]
+            export_bookmark(repo, bmark)
         elif ref.startswith('refs/tags/'):
             tag = ref[len('refs/tags/'):]
             export_tag(repo, tag)
@@ -319,7 +332,7 @@ def do_import(parser):
     print 'done'
 
 def main(args):
-    global prefix, dirname, marks, branches
+    global prefix, dirname, marks, branches, bmarks
 
     alias = args[1]
     url = args[2]
@@ -327,6 +340,7 @@ def main(args):
     gitdir = os.environ['GIT_DIR']
     dirname = os.path.join(gitdir, 'hg', alias)
     branches = {}
+    bmarks = {}
 
     repo = get_repo(url, alias)
     prefix = 'refs/hg/%s' % alias
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 01/13] Add new remote-hg transport helper
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras
In-Reply-To: <1351396453-29042-1-git-send-email-felipe.contreras@gmail.com>

Signed-off-by: Felipe Contreras <felipe.contreras@gmail.com>
---
 contrib/remote-hg/git-remote-hg | 356 ++++++++++++++++++++++++++++++++++++++++
 1 file changed, 356 insertions(+)
 create mode 100755 contrib/remote-hg/git-remote-hg

diff --git a/contrib/remote-hg/git-remote-hg b/contrib/remote-hg/git-remote-hg
new file mode 100755
index 0000000..c771182
--- /dev/null
+++ b/contrib/remote-hg/git-remote-hg
@@ -0,0 +1,356 @@
+#!/usr/bin/python
+
+# Inspired by Rocco Rutte's hg-fast-export
+
+# Just copy to your ~/bin, or anywhere in your $PATH.
+# Then you can clone with:
+# git clone hg::/path/to/mercurial/repo/
+
+from mercurial import hg, ui
+
+import re
+import sys
+import os
+import json
+
+NAME_RE = re.compile('^([^<>]+)')
+AUTHOR_RE = re.compile('^([^<>]+?)? ?<([^<>]+)>$')
+
+def die(msg, *args):
+    sys.stderr.write('ERROR: %s\n' % (msg % args))
+    sys.exit(1)
+
+def warn(msg, *args):
+    sys.stderr.write('WARNING: %s\n' % (msg % args))
+
+def gitmode(flags):
+    return 'l' in flags and '120000' or 'x' in flags and '100755' or '100644'
+
+def gittz(tz):
+    return '%+03d%02d' % (-tz / 3600, -tz % 3600 / 60)
+
+class Marks:
+
+    def __init__(self, path):
+        self.path = path
+        self.tips = {}
+        self.marks = {}
+        self.last_mark = 0
+
+        self.load()
+
+    def load(self):
+        if not os.path.exists(self.path):
+            return
+
+        tmp = json.load(open(self.path))
+
+        self.tips = tmp['tips']
+        self.marks = tmp['marks']
+        self.last_mark = tmp['last-mark']
+
+    def dict(self):
+        return { 'tips': self.tips, 'marks': self.marks, 'last-mark' : self.last_mark }
+
+    def store(self):
+        json.dump(self.dict(), open(self.path, 'w'))
+
+    def __str__(self):
+        return str(self.dict())
+
+    def from_rev(self, rev):
+        return self.marks[str(rev)]
+
+    def next_mark(self, rev):
+        self.last_mark += 1
+        self.marks[str(rev)] = self.last_mark
+        return self.last_mark
+
+    def is_marked(self, rev):
+        return self.marks.has_key(str(rev))
+
+    def get_tip(self, branch):
+        return self.tips.get(branch, 0)
+
+    def set_tip(self, branch, tip):
+        self.tips[branch] = tip
+
+class Parser:
+
+    def __init__(self, repo):
+        self.repo = repo
+        self.line = self.get_line()
+
+    def get_line(self):
+        return sys.stdin.readline().strip()
+
+    def __getitem__(self, i):
+        return self.line.split()[i]
+
+    def check(self, word):
+        return self.line.startswith(word)
+
+    def each_block(self, separator):
+        while self.line != separator:
+            yield self.line
+            self.line = self.get_line()
+
+    def __iter__(self):
+        return self.each_block('')
+
+    def next(self):
+        self.line = self.get_line()
+        if self.line == 'done':
+            self.line = None
+
+def export_file(fc):
+    d = fc.data()
+    print "M %s inline %s" % (gitmode(fc.flags()), fc.path())
+    print "data %d" % len(d)
+    print d
+
+def get_filechanges(repo, ctx, parents):
+    l = [repo.status(p, ctx)[:3] for p in parents]
+    changed, added, removed = [set(sum(e, [])) for e in zip(*l)]
+    return added | changed, removed
+
+def fixup_user(user):
+    user = user.replace('"', '')
+    name = mail = None
+    m = AUTHOR_RE.match(user)
+    if m:
+        name = m.group(1)
+        mail = m.group(2).strip()
+    else:
+        m = NAME_RE.match(user)
+        if m:
+            name = m.group(1).strip()
+
+    if not name:
+        name = 'Unknown'
+    if not mail:
+        mail = 'unknown'
+
+    return '%s <%s>' % (name, mail)
+
+def get_repo(url, alias):
+    global dirname
+
+    myui = ui.ui()
+    myui.setconfig('ui', 'interactive', 'off')
+
+    if hg.islocal(url):
+        repo = hg.repository(myui, url)
+    else:
+        local_path = os.path.join(dirname, 'clone')
+        if not os.path.exists(local_path):
+            peer, dstpeer = hg.clone(myui, {}, url, local_path, update=False, pull=True)
+            repo = dstpeer.local()
+        else:
+            repo = hg.repository(myui, local_path)
+            peer = hg.peer(myui, {}, url)
+            repo.pull(peer, heads=None, force=True)
+
+    return repo
+
+def rev_to_mark(rev):
+    global marks
+    return marks.from_rev(rev)
+
+def export_ref(repo, name, kind, head):
+    global prefix, marks
+
+    ename = '%s/%s' % (kind, name)
+    tip = marks.get_tip(ename)
+
+    # mercurial takes too much time checking this
+    if tip and tip == head.rev():
+        # nothing to do
+        return
+    revs = repo.revs('%u:%u' % (tip, head))
+    count = 0
+
+    revs = [rev for rev in revs if not marks.is_marked(rev)]
+
+    for rev in revs:
+
+        c = repo[rev]
+        (manifest, user, (time, tz), files, desc, extra) = repo.changelog.read(c.node())
+        rev_branch = extra['branch']
+
+        author = "%s %d %s" % (fixup_user(user), time, gittz(tz))
+        if 'committer' in extra:
+            user, time, tz = extra['committer'].rsplit(' ', 2)
+            committer = "%s %s %s" % (user, time, gittz(int(tz)))
+        else:
+            committer = author
+
+        parents = [p for p in repo.changelog.parentrevs(rev) if p >= 0]
+
+        if len(parents) == 0:
+            modified = c.manifest().keys()
+            removed = []
+        else:
+            modified, removed = get_filechanges(repo, c, parents)
+
+        if len(parents) == 0 and rev:
+            print 'reset %s/%s' % (prefix, ename)
+
+        print "commit %s/%s" % (prefix, ename)
+        print "mark :%d" % (marks.next_mark(rev))
+        print "author %s" % (author)
+        print "committer %s" % (committer)
+        print "data %d" % (len(desc))
+        print desc
+
+        if len(parents) > 0:
+            print "from :%s" % (rev_to_mark(parents[0]))
+            if len(parents) > 1:
+                print "merge :%s" % (rev_to_mark(parents[1]))
+
+        for f in removed:
+            print "D %s" % (f)
+        for f in modified:
+            export_file(c.filectx(f))
+        print
+
+        count += 1
+        if (count % 100 == 0):
+            print "progress revision %d '%s' (%d/%d)" % (rev, ename, count, len(revs))
+            print "#############################################################"
+
+    # make sure the ref is updated
+    print "reset %s/%s" % (prefix, ename)
+    print "from :%u" % rev_to_mark(rev)
+    print
+
+    marks.set_tip(ename, rev)
+
+def export_tag(repo, tag):
+    export_ref(repo, tag, 'tags', repo[tag])
+
+def export_branch(repo, branch):
+    tip = get_branch_tip(repo, branch)
+    head = repo[tip]
+    export_ref(repo, branch, 'branches', head)
+
+def export_head(repo):
+    global g_head
+    export_ref(repo, g_head[0], g_head[1], g_head[2])
+
+def do_capabilities(parser):
+    global prefix, dirname
+
+    print "import"
+    print "refspec refs/heads/branches/*:%s/branches/*" % prefix
+    print "refspec refs/tags/*:%s/tags/*" % prefix
+    print
+
+def get_branch_tip(repo, branch):
+    global branches
+
+    heads = branches.get(branch, None)
+    if not heads:
+        return None
+
+    # verify there's only one head
+    if (len(heads) > 1):
+        warn("Branch '%s' has more than one head, consider merging" % branch)
+        return repo.branchtip(branch)
+
+    return heads[0]
+
+def list_branch_head(repo, cur):
+    global g_head
+
+    tip = get_branch_tip(repo, cur)
+    head = 'branches/' + cur
+    print "@refs/heads/%s HEAD" % head
+    g_head = (head, 'branches', repo[tip])
+
+def do_list(parser):
+    global branches
+
+    repo = parser.repo
+    for branch in repo.branchmap():
+        heads = repo.branchheads(branch)
+        if len(heads):
+            branches[branch] = heads
+
+    cur = repo.dirstate.branch()
+
+    list_branch_head(repo, cur)
+    for branch in branches:
+        print "? refs/heads/branches/%s" % branch
+
+    for tag, node in repo.tagslist():
+        if tag == 'tip':
+            continue
+        print "? refs/tags/%s" % tag
+
+    print
+
+def do_import(parser):
+    repo = parser.repo
+
+    path = os.path.join(dirname, 'marks-git')
+
+    print "feature done"
+    if os.path.exists(path):
+        print "feature import-marks=%s" % path
+    print "feature export-marks=%s" % path
+    sys.stdout.flush()
+
+    # lets get all the import lines
+    while parser.check('import'):
+        ref = parser[1]
+
+        if (ref == 'HEAD'):
+            export_head(repo)
+        elif ref.startswith('refs/heads/branches/'):
+            branch = ref[len('refs/heads/branches/'):]
+            export_branch(repo, branch)
+        elif ref.startswith('refs/tags/'):
+            tag = ref[len('refs/tags/'):]
+            export_tag(repo, tag)
+
+        parser.next()
+
+    print 'done'
+
+def main(args):
+    global prefix, dirname, marks, branches
+
+    alias = args[1]
+    url = args[2]
+
+    gitdir = os.environ['GIT_DIR']
+    dirname = os.path.join(gitdir, 'hg', alias)
+    branches = {}
+
+    repo = get_repo(url, alias)
+    prefix = 'refs/hg/%s' % alias
+
+    if not os.path.exists(dirname):
+        os.makedirs(dirname)
+
+    marks_path = os.path.join(dirname, 'marks-hg')
+    marks = Marks(marks_path)
+
+    parser = Parser(repo)
+    for line in parser:
+        if parser.check('capabilities'):
+            do_capabilities(parser)
+        elif parser.check('list'):
+            do_list(parser)
+        elif parser.check('import'):
+            do_import(parser)
+        elif parser.check('export'):
+            do_export(parser)
+        else:
+            die('unhandled command: %s' % line)
+        sys.stdout.flush()
+
+    marks.store()
+
+sys.exit(main(sys.argv))
-- 
1.8.0

^ permalink raw reply related

* [PATCH v4 00/13] New remote-hg helper
From: Felipe Contreras @ 2012-10-28  3:54 UTC (permalink / raw)
  To: git
  Cc: Junio C Hamano, Sverre Rabbelier, Johannes Schindelin,
	Ilari Liusvaara, Daniel Barkalow, Jeff King, Michael J Gruber,
	Felipe Contreras

Hi,

I've ported the tests from hg-git and made sure that the output from remote-hg
matches the output of hg-git. With these extensive tests I would consider this
one ready for wide use. Not only do the tests pass, I've compared the generated
repos of a few projects, and the SHA-1's are exactly the same :)

This remote-hg has advantages other tools don't have:

 * Uses transport-helper (git clone hg::path)
 * The code is small
 * The code is simple
 * No external dependencies (other than mercurial)
 * It's easy to install (put into your path)
 * Has extensive tests
 * Active development
 * Has compatibility with hg-git
 * The required patches are available
 * No changes necesary to git core

One important alternative is the one written by Sverre Rabbelier that is now
maintained and distributed in msysgit. It's hard to evaluate this option as
there isn't a branch specific to this remote helper so it would be possible to
evaluate the necessary patches.

Changes since v3:

 * New extensive tests
 * Add compatibility mode with hg-git
 * Added support for boomkars
 * Add mercurial information to the git msg (branch, renames, extra, etc.)
 * Properly handle HEAD
 * Fix author/committer information
 * Implement 'done' feature for error handling
 * Restore hg user properly
 * Set file correct modes
 * Match hg merge behavior
 * Prefix hg branches
 * Encoding fixes
 * Stricter parser
 * Support for 'reset' command
 * Fix support for URL pushing (unaliased)

Changes since v2:

 * Added support for pushing
 * Tests copied from original remote-hg
 * Custom default -> master renames removed
 * Code reorganized

Changes since v1:

 * Improved documentation
 * Use more common 'python' binary
 * Warn, don't barf when a branch has multiple heads
 * Fixed marks to fetch after cloned
 * Support for cloning/pulling remote repositories
 * Use a more appropriate internal directory (e.g. .git/hg/origin)
 * Fixes for python3

Felipe Contreras (13):
  Add new remote-hg transport helper
  remote-hg: add support for bookmarks
  remote-hg: add support for pushing
  remote-hg: add support for remote pushing
  remote-hg: add support to push URLs
  remote-hg: make sure the encoding is correct
  remote-hg: match hg merge behavior
  remote-hg: add support for hg-git compat mode
  remote-hg: add compat for hg-git author fixes
  remote-hg: fake bookmark when there's none
  remote-hg: add support for fake remote
  remote-hg: add tests to compare with hg-git
  remote-hg: add extra author test

 contrib/remote-hg/git-remote-hg | 770 ++++++++++++++++++++++++++++++++++++++++
 t/t5802-remote-hg-hg-git.sh     | 449 +++++++++++++++++++++++
 2 files changed, 1219 insertions(+)
 create mode 100755 contrib/remote-hg/git-remote-hg
 create mode 100755 t/t5802-remote-hg-hg-git.sh

-- 
1.8.0

^ permalink raw reply


This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox