[PATCH] Warnings before rebasing -i published history

Subsystems: the rest

STALE3737d

5 messages, 2 authors, 2016-06-15 · open the first message on its own page

[PATCH] Warnings before rebasing -i published history

From: Lucien Kong <hidden>
Date: 2016-06-15 22:54:00

"git rebase -i" can be very dangerous if used on an already published
history. This code detects that one is rewriting a commit that is an
ancestor of a remote-tracking branch, and warns the user through the
editor.

Signed-off-by: Lucien Kong <redacted>
Signed-off-by: Valentin Duperray <redacted>
Signed-off-by: Franck Jonas <redacted>
Signed-off-by: Thomas Nguy <redacted>
Signed-off-by: Huynh Khoi Nguyen Nguyen <redacted>
Signed-off-by: Matthieu Moy <redacted>
---
For now, the code only warns the user; he can't revert back to his
original state. We agree with everyone that he should be given a
way to abort the rebase.

 git-rebase--interactive.sh    |   17 +++++++++++++++++
 t/t3404-rebase-interactive.sh |   26 ++++++++++++++++++++++++++
 2 files changed, 43 insertions(+), 0 deletions(-)
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 0c19b7c..67b5faf 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -684,6 +684,20 @@ rearrange_squash () {
 	rm -f "$1.sq" "$1.rearranged"
 }
 
+# Add a warning notification at the end of each pick or fixup/squash
+# line of the todo list, providing the picking commit is already
+# published.
+warn_published () {
+	cat "$1" | while read -r command sha1 message
+	do
+		test -n "$sha1" || break
+		if test -n "$(git branch -r --contains "$sha1")"
+		then
+			printf "%s\n" "$(sed -e "/"$sha1"/ s|$| [Published]|" "$1")" >"$1"
+		fi
+	done
+}
+
 case "$action" in
 continue)
 	# do we have anything to commit?
@@ -857,6 +871,7 @@ fi
 
 test -s "$todo" || echo noop >> "$todo"
 test -n "$autosquash" && rearrange_squash "$todo"
+warn_published "$todo"
 cat >> "$todo" << EOF
 
 # Rebase $shortrevisions onto $shortonto
@@ -869,6 +884,8 @@ cat >> "$todo" << EOF
 #  f, fixup = like "squash", but discard this commit's log message
 #  x, exec = run command (the rest of the line) using shell
 #
+# Warning: [Published] means that the commit has already been published
+#
 # These lines can be re-ordered; they are executed from top to bottom.
 #
 # If you remove a line here THAT COMMIT WILL BE LOST.
diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
index 025c1c6..f7c31c1 100755
--- a/t/t3404-rebase-interactive.sh
+++ b/t/t3404-rebase-interactive.sh
@@ -755,4 +755,30 @@ test_expect_success 'rebase-i history with funny messages' '
 	test_cmp expect actual
 '
 
+test_expect_success 'warn before rewriting published history' '
+	test_when_finished "rm -rf git.git git" &&
+	git init git.git &&
+	git clone git &&
+	(
+		cd git &&
+		test_commit one_commit main.txt one_commit &&
+		test_commit two_commit main.txt two_commit &&
+		test_commit three_commit main.txt three_commit &&
+		git push --all &&
+		test_commit four_commit main.txt four_commit &&
+		FAKE_LINES="1 2 3" &&
+		export FAKE_LINES &&
+		tmp=$(git rebase -i HEAD~3 | sed -n 2,4p) &&
+		echo "$tmp" >actual &&
+		tmp=$(git cherry --abbrev=7 HEAD~3 | sed -e 's/+[[:space:]]//g') &&
+		two_sha1=$(echo "$tmp" | sed -n 1p) &&
+		three_sha1=$(echo "$tmp" | sed -n 2p) &&
+		four_sha1=$(echo "$tmp" | sed -n 3p) &&
+		echo "pick $two_sha1 two_commit [Published]" >expected &&
+		echo "pick $three_sha1 three_commit [Published]" >>expected &&
+		echo "pick $four_sha1 four_commit" >>expected &&
+		test_cmp expected actual
+	)
+'
+
 test_done
-- 
1.7.8

[PATCHv2] Warnings before rebasing -i published history

From: Lucien Kong <hidden>
Date: 2016-06-15 22:54:01

"git rebase -i" can be very dangerous if used on an already published
history. This code detects that one is rewriting a commit that is an
ancestor of a remote-tracking branch, and warns the user through the
editor. This feature is controlled by a new config key
rebase.checkremoterefs.

Signed-off-by: Lucien Kong <redacted>
Signed-off-by: Valentin Duperray <redacted>
Signed-off-by: Franck Jonas <redacted>
Signed-off-by: Thomas Nguy <redacted>
Signed-off-by: Huynh Khoi Nguyen Nguyen <redacted>
Signed-off-by: Matthieu Moy <redacted>
---
 Documentation/config.txt      |    5 +++++
 Documentation/git-rebase.txt  |    5 +++++
 git-rebase--interactive.sh    |   22 ++++++++++++++++++++++
 git-rebase.sh                 |    2 ++
 t/lib-rebase.sh               |   11 +++++++++++
 t/t3404-rebase-interactive.sh |   29 +++++++++++++++++++++++++++++
 6 files changed, 74 insertions(+), 0 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 915cb5a..e47f6e0 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1753,6 +1753,11 @@ rebase.stat::
 rebase.autosquash::
 	If set to true enable '--autosquash' option by default.
 
+rebase.checkremoterefs::
+	If it is set to 'true', git rebase -i will show after each
+	commit ancestor of a remote-tracking branch the name of these
+	branches through the editor.
+
 receive.autogc::
 	By default, git-receive-pack will run "git-gc --auto" after
 	receiving data from git-push and updating refs.  You can stop
diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index 147fa1a..b68a80b 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -208,6 +208,11 @@ rebase.stat::
 rebase.autosquash::
 	If set to true enable '--autosquash' option by default.
 
+rebase.checkremoterefs::
+	If it is set to 'true', git rebase -i will show after each
+	commit ancestor of a remote-tracking branch the name of these
+	branches through the editor.
+
 OPTIONS
 -------
 <newbase>::
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 0c19b7c..ad6f8a7 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -684,6 +684,27 @@ rearrange_squash () {
 	rm -f "$1.sq" "$1.rearranged"
 }
 
+# Add the name the branches after each pick, fixup or squash commit that
+# is an ancestor of a remote-tracking branch.
+add_remoterefs () {
+	while read -r command sha1 message
+	do
+		printf '%s\n' "$command $sha1 $message"
+		git branch -r --contains "$sha1" >"$1.branch"
+		if test -s "$1.branch"
+		then
+			printf "# Commit above this line appear in:"
+			while read -r branch
+			do
+				printf " $branch"
+			done <"$1.branch"
+			printf '\n'
+		fi
+	done >"$1.published" <"$1"
+	cat "$1.published" >"$1"
+	rm -f "$1.published" "$1.branch"
+}
+
 case "$action" in
 continue)
 	# do we have anything to commit?
@@ -857,6 +878,7 @@ fi
 
 test -s "$todo" || echo noop >> "$todo"
 test -n "$autosquash" && rearrange_squash "$todo"
+test -n "$checkremoterefs" && add_remoterefs "$todo"
 cat >> "$todo" << EOF
 
 # Rebase $shortrevisions onto $shortonto
diff --git a/git-rebase.sh b/git-rebase.sh
index e616737..f8675b5 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -99,7 +99,9 @@ action=
 preserve_merges=
 autosquash=
 keep_empty=
+checkremoterefs=
 test "$(git config --bool rebase.autosquash)" = "true" && autosquash=t
+test "$(git config --bool rebase.checkremoterefs)" = "true" && checkremoterefs=t
 
 read_basic_state () {
 	head_name=$(cat "$state_dir"/head-name) &&
diff --git a/t/lib-rebase.sh b/t/lib-rebase.sh
index 6ccf797..4906867 100644
--- a/t/lib-rebase.sh
+++ b/t/lib-rebase.sh
@@ -65,3 +65,14 @@ EOF
 	test_set_editor "$(pwd)/fake-editor.sh"
 	chmod a+x fake-editor.sh
 }
+
+
+set_copy_editor () {
+	echo "#!$SHELL_PATH" >editor.sh
+	cat >> editor.sh <<\EOF
+cat "$1" >"$TODO_DUMP"
+EOF
+
+	test_set_editor "$(pwd)/editor.sh"
+	chmod a+x editor.sh
+}
diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
index 025c1c6..f99c0c2 100755
--- a/t/t3404-rebase-interactive.sh
+++ b/t/t3404-rebase-interactive.sh
@@ -755,4 +755,33 @@ test_expect_success 'rebase-i history with funny messages' '
 	test_cmp expect actual
 '
 
+test_expect_success 'warn before rewriting published history' '
+	test_when_finished "rm -rf git.git git" &&
+	git init git.git &&
+	git clone git &&
+	(
+		cd git &&
+		git config rebase.checkremoterefs true &&
+		test_commit one_commit main.txt one_commit &&
+		test_commit two_commit main.txt two_commit &&
+		test_commit three_commit main.txt three_commit &&
+		git push --all &&
+		test_commit four_commit main.txt four_commit &&
+		set_copy_editor &&
+		TODO_DUMP=actual EDITOR=./editor.sh git rebase -i HEAD~3 &&
+		tmp=$(cat actual | sed -n 1,5p) &&
+		echo "$tmp" >actual &&
+		tmp=$(git cherry --abbrev=7 HEAD~3 | sed -e 's/+[[:space:]]//g') &&
+		two_sha1=$(echo "$tmp" | sed -n 1p) &&
+		three_sha1=$(echo "$tmp" | sed -n 2p) &&
+		four_sha1=$(echo "$tmp" | sed -n 3p) &&
+		echo "pick $two_sha1 two_commit" >expected &&
+		echo "# Commit above this line appear in: origin/master" >>expected &&
+		echo "pick $three_sha1 three_commit" >>expected &&
+		echo "# Commit above this line appear in: origin/master" >>expected &&
+		echo "pick $four_sha1 four_commit" >>expected &&
+		test_cmp expected actual
+	)
+'
+
 test_done
-- 
1.7.8

[PATCHv3 2/2] Warnings before amending published history

From: Lucien Kong <hidden>
Date: 2016-06-15 22:54:02

This code detects that one is rewriting a commit that is an ancestor
of a remote-tracking branch with "git commit --amend", and warns the
user through the editor.

Signed-off-by: Lucien Kong <redacted>
Signed-off-by: Valentin Duperray <redacted>
Signed-off-by: Franck Jonas <redacted>
Signed-off-by: Thomas Nguy <redacted>
Signed-off-by: Huynh Khoi Nguyen Nguyen <redacted>
Signed-off-by: Matthieu Moy <redacted>
---
At this point, this feature is not controlled by the config key
rebase.checkremoterefs. Also, the code can only detects the commits
that were published on the same current branch.

 builtin/commit.c              |   82 ++++++++++++++++++++++++++
 sha1_name.c                   |   95 +++++++-----------------------
 sha1_name.h                   |  130 +++++++++++++++++++++++++++++++++++++++++
 t/t3404-rebase-interactive.sh |   65 ++++++++++++++++++++
 4 files changed, 298 insertions(+), 74 deletions(-)
 create mode 100644 sha1_name.h
diff --git a/builtin/commit.c b/builtin/commit.c
index f43eaaf..53fe120 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -28,6 +28,7 @@
 #include "submodule.h"
 #include "gpg-interface.h"
 #include "column.h"
+#include "sha1_name.h"
 
 static const char * const builtin_commit_usage[] = {
 	"git commit [options] [--] <filepattern>...",
@@ -584,6 +585,83 @@ static char *cut_ident_timestamp_part(char *string)
 	return ket;
 }
 
+static char *read_line_from_git_path(const char *filename)
+{
+	struct strbuf buf = STRBUF_INIT;
+	FILE *fp = fopen(git_path("%s", filename), "r");
+	if (!fp) {
+		strbuf_release(&buf);
+		return NULL;
+	}
+	strbuf_getline(&buf, fp, '\n');
+	if (!fclose(fp))
+		return strbuf_detach(&buf, NULL);
+	else
+		return NULL;
+}
+
+static int insert_first_line_file(char *to_write, char *file_to_modify)
+{
+	int c;
+	FILE *tmp = tmpfile();
+	FILE *file = fopen(file_to_modify, "r+");
+	if (!file || !tmp)
+		return 0;
+
+	while ((c = fgetc(file)) != EOF)
+		fputc(c, tmp);
+
+	rewind(file);
+	rewind(tmp);
+	fputs(to_write, file);
+	while ((c = fgetc(tmp)) != EOF)
+		fputc(c, file);
+
+	fclose(tmp);
+	fclose(file);
+	return 1;
+}
+
+static int amend_warn_published(void)
+{
+	char *head_path = read_line_from_git_path("HEAD");
+	char *last_commit_sha1;
+	char remote_path[PATH_MAX] = "refs/remotes/origin/";
+	char *remote_branch;
+	unsigned char nth_ancestor_remote_sha1[20];
+	int i = 0;
+
+	if (!head_path)
+		return 0;
+
+	strtok(head_path, " ");
+	last_commit_sha1 = read_line_from_git_path(strtok(NULL, ""));
+	if (!last_commit_sha1)
+		return 0;
+
+	remote_branch = read_line_from_git_path("HEAD");
+	if (!remote_branch)
+		return 0;
+
+	strtok(remote_branch, "/");
+	strtok(NULL, "/");
+	strcat(remote_path, strtok(NULL, ""));
+
+	while (!get_nth_ancestor(remote_path, 40, nth_ancestor_remote_sha1, i)) {
+		if (!strcmp(sha1_to_hex(nth_ancestor_remote_sha1), last_commit_sha1)) {
+			insert_first_line_file("# The commit to reword is already published.\n\n",
+					git_path(commit_editmsg));
+			break;
+		}
+		i++;
+	}
+
+	free(head_path);
+	free(last_commit_sha1);
+	free(remote_branch);
+	return 1;
+}
+
 static int prepare_to_commit(const char *index_file, const char *prefix,
 			     struct commit *current_head,
 			     struct wt_status *s,
@@ -840,6 +918,10 @@ static int prepare_to_commit(const char *index_file, const char *prefix,
 		const char *env[2] = { NULL };
 		env[0] =  index;
 		snprintf(index, sizeof(index), "GIT_INDEX_FILE=%s", index_file);
+
+		if (amend)
+			amend_warn_published();
+
 		if (launch_editor(git_path(commit_editmsg), NULL, env)) {
 			fprintf(stderr,
 			_("Please supply the message using either -m or -F option.\n"));
diff --git a/sha1_name.c b/sha1_name.c
index c633113..14a0e96 100644
--- a/sha1_name.c
+++ b/sha1_name.c
@@ -1,4 +1,5 @@
 #include "cache.h"
+#include "sha1_name.h"
 #include "tag.h"
 #include "commit.h"
 #include "tree.h"
@@ -7,9 +8,7 @@
 #include "refs.h"
 #include "remote.h"
 
-static int get_sha1_oneline(const char *, unsigned char *, struct commit_list *);
-
-static int find_short_object_filename(int len, const char *name, unsigned char *sha1)
+int find_short_object_filename(int len, const char *name, unsigned char *sha1)
 {
 	struct alternate_object_database *alt;
 	char hex[40];
@@ -56,7 +55,7 @@ static int find_short_object_filename(int len, const char *name, unsigned char *
 	return found;
 }
 
-static int match_sha(unsigned len, const unsigned char *a, const unsigned char *b)
+int match_sha(unsigned len, const unsigned char *a, const unsigned char *b)
 {
 	do {
 		if (*a != *b)
@@ -71,7 +70,7 @@ static int match_sha(unsigned len, const unsigned char *a, const unsigned char *
 	return 1;
 }
 
-static int find_short_packed_object(int len, const unsigned char *match, unsigned char *sha1)
+int find_short_packed_object(int len, const unsigned char *match, unsigned char *sha1)
 {
 	struct packed_git *p;
 	const unsigned char *found_sha1 = NULL;
@@ -130,10 +129,7 @@ static int find_short_packed_object(int len, const unsigned char *match, unsigne
 	return found;
 }
 
-#define SHORT_NAME_NOT_FOUND (-1)
-#define SHORT_NAME_AMBIGUOUS (-2)
-
-static int find_unique_short_object(int len, char *canonical,
+int find_unique_short_object(int len, char *canonical,
 				    unsigned char *res, unsigned char *sha1)
 {
 	int has_unpacked, has_packed;
@@ -157,7 +153,7 @@ static int find_unique_short_object(int len, char *canonical,
 	return 0;
 }
 
-static int get_short_sha1(const char *name, int len, unsigned char *sha1,
+int get_short_sha1(const char *name, int len, unsigned char *sha1,
 			  int quietly)
 {
 	int i, status;
@@ -216,7 +212,7 @@ const char *find_unique_abbrev(const unsigned char *sha1, int len)
 	return hex;
 }
 
-static int ambiguous_path(const char *path, int len)
+int ambiguous_path(const char *path, int len)
 {
 	int slash = 1;
 	int cnt;
@@ -241,7 +237,7 @@ static int ambiguous_path(const char *path, int len)
 	return slash;
 }
 
-static inline int upstream_mark(const char *string, int len)
+inline int upstream_mark(const char *string, int len)
 {
 	const char *suffix[] = { "@{upstream}", "@{u}" };
 	int i;
@@ -255,9 +251,7 @@ static inline int upstream_mark(const char *string, int len)
 	return 0;
 }
 
-static int get_sha1_1(const char *name, int len, unsigned char *sha1);
-
-static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
+int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 {
 	static const char *warn_msg = "refname '%.*s' is ambiguous.";
 	char *real_ref = NULL;
@@ -358,7 +352,7 @@ static int get_sha1_basic(const char *str, int len, unsigned char *sha1)
 	return 0;
 }
 
-static int get_parent(const char *name, int len,
+int get_parent(const char *name, int len,
 		      unsigned char *result, int idx)
 {
 	unsigned char sha1[20];
@@ -388,7 +382,7 @@ static int get_parent(const char *name, int len,
 	return -1;
 }
 
-static int get_nth_ancestor(const char *name, int len,
+int get_nth_ancestor(const char *name, int len,
 			    unsigned char *result, int generation)
 {
 	unsigned char sha1[20];
@@ -436,7 +430,7 @@ struct object *peel_to_type(const char *name, int namelen,
 	}
 }
 
-static int peel_onion(const char *name, int len, unsigned char *sha1)
+int peel_onion(const char *name, int len, unsigned char *sha1)
 {
 	unsigned char outer[20];
 	const char *sp;
@@ -522,7 +516,7 @@ static int peel_onion(const char *name, int len, unsigned char *sha1)
 	return 0;
 }
 
-static int get_describe_name(const char *name, int len, unsigned char *sha1)
+int get_describe_name(const char *name, int len, unsigned char *sha1)
 {
 	const char *cp;
 
@@ -542,7 +536,7 @@ static int get_describe_name(const char *name, int len, unsigned char *sha1)
 	return -1;
 }
 
-static int get_sha1_1(const char *name, int len, unsigned char *sha1)
+int get_sha1_1(const char *name, int len, unsigned char *sha1)
 {
 	int ret, has_suffix;
 	const char *cp;
@@ -590,17 +584,7 @@ static int get_sha1_1(const char *name, int len, unsigned char *sha1)
 	return get_short_sha1(name, len, sha1, 0);
 }
 
-/*
- * This interprets names like ':/Initial revision of "git"' by searching
- * through history and returning the first commit whose message starts
- * the given regular expression.
- *
- * For future extension, ':/!' is reserved. If you want to match a message
- * beginning with a '!', you have to repeat the exclamation mark.
- */
-#define ONELINE_SEEN (1u<<20)
-
-static int handle_one_ref(const char *path,
+int handle_one_ref(const char *path,
 		const unsigned char *sha1, int flag, void *cb_data)
 {
 	struct commit_list **list = cb_data;
@@ -618,7 +602,7 @@ static int handle_one_ref(const char *path,
 	return 0;
 }
 
-static int get_sha1_oneline(const char *prefix, unsigned char *sha1,
+int get_sha1_oneline(const char *prefix, unsigned char *sha1,
 			    struct commit_list *list)
 {
 	struct commit_list *backup = NULL, *l;
@@ -675,12 +659,7 @@ static int get_sha1_oneline(const char *prefix, unsigned char *sha1,
 	return found ? 0 : -1;
 }
 
-struct grab_nth_branch_switch_cbdata {
-	long cnt, alloc;
-	struct strbuf *buf;
-};
-
-static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
+int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 				  const char *email, unsigned long timestamp, int tz,
 				  const char *message, void *cb_data)
 {
@@ -704,11 +683,7 @@ static int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
 	return 0;
 }
 
-/*
- * Parse @{-N} syntax, return the number of characters parsed
- * if successful; otherwise signal an error with negative value.
- */
-static int interpret_nth_prior_checkout(const char *name, struct strbuf *buf)
+int interpret_nth_prior_checkout(const char *name, struct strbuf *buf)
 {
 	long nth;
 	int i, retval;
@@ -794,27 +769,6 @@ int get_sha1_mb(const char *name, unsigned char *sha1)
 	return st;
 }
 
-/*
- * This reads short-hand syntax that not only evaluates to a commit
- * object name, but also can act as if the end user spelled the name
- * of the branch from the command line.
- *
- * - "@{-N}" finds the name of the Nth previous branch we were on, and
- *   places the name of the branch in the given buf and returns the
- *   number of characters parsed if successful.
- *
- * - "<branch>@{upstream}" finds the name of the other ref that
- *   <branch> is configured to merge with (missing <branch> defaults
- *   to the current branch), and places the name of the branch in the
- *   given buf and returns the number of characters parsed if
- *   successful.
- *
- * If the input is not of the accepted format, it returns a negative
- * number to signal an error.
- *
- * If the input was ok but there are not N branch switches in the
- * reflog, it returns 0.
- */
 int interpret_branch_name(const char *name, struct strbuf *buf)
 {
 	char *cp;
@@ -898,18 +852,13 @@ int strbuf_check_branch_ref(struct strbuf *sb, const char *name)
 	return check_refname_format(sb->buf, 0);
 }
 
-/*
- * This is like "get_sha1_basic()", except it allows "sha1 expressions",
- * notably "xyz^" for "parent of xyz"
- */
 int get_sha1(const char *name, unsigned char *sha1)
 {
 	struct object_context unused;
 	return get_sha1_with_context(name, sha1, &unused);
 }
 
-/* Must be called only when object_name:filename doesn't exist. */
-static void diagnose_invalid_sha1_path(const char *prefix,
+void diagnose_invalid_sha1_path(const char *prefix,
 				       const char *filename,
 				       const unsigned char *tree_sha1,
 				       const char *object_name)
@@ -946,8 +895,7 @@ static void diagnose_invalid_sha1_path(const char *prefix,
 	}
 }
 
-/* Must be called only when :stage:filename doesn't exist. */
-static void diagnose_invalid_index_path(int stage,
+void diagnose_invalid_index_path(int stage,
 					const char *prefix,
 					const char *filename)
 {
@@ -1003,7 +951,6 @@ static void diagnose_invalid_index_path(int stage,
 	free(fullname);
 }
 
-
 int get_sha1_with_mode_1(const char *name, unsigned char *sha1, unsigned *mode,
 			 int only_to_die, const char *prefix)
 {
@@ -1014,7 +961,7 @@ int get_sha1_with_mode_1(const char *name, unsigned char *sha1, unsigned *mode,
 	return ret;
 }
 
-static char *resolve_relative_path(const char *rel)
+char *resolve_relative_path(const char *rel)
 {
 	if (prefixcmp(rel, "./") && prefixcmp(rel, "../"))
 		return NULL;
diff --git a/sha1_name.h b/sha1_name.h
new file mode 100644
index 0000000..bdbcecd
--- /dev/null
+++ b/sha1_name.h
@@ -0,0 +1,130 @@
+#ifndef SHA1_NAME_H
+#define SHA1_NAME_H
+
+#include "commit.h"
+
+int find_short_object_filename(int len, const char *name, unsigned char *sha1);
+
+int match_sha(unsigned len, const unsigned char *a, const unsigned char *b);
+
+int find_short_packed_object(int len, const unsigned char *match, unsigned char *sha1);
+
+#define SHORT_NAME_NOT_FOUND (-1)
+#define SHORT_NAME_AMBIGUOUS (-2)
+
+int find_unique_short_object(int len, char *canonical,
+				    unsigned char *res, unsigned char *sha1);
+
+int get_short_sha1(const char *name, int len, unsigned char *sha1,
+			  int quietly);
+
+const char *find_unique_abbrev(const unsigned char *sha1, int len);
+
+int ambiguous_path(const char *path, int len);
+
+inline int upstream_mark(const char *string, int len);
+
+int get_sha1_basic(const char *str, int len, unsigned char *sha1);
+
+int get_parent(const char *name, int len,
+		      unsigned char *result, int idx);
+
+int get_nth_ancestor(const char *name, int len,
+			    unsigned char *result, int generation);
+
+struct object *peel_to_type(const char *name, int namelen,
+			    struct object *o, enum object_type expected_type);
+
+int peel_onion(const char *name, int len, unsigned char *sha1);
+
+int get_describe_name(const char *name, int len, unsigned char *sha1);
+
+int get_sha1_1(const char *name, int len, unsigned char *sha1);
+
+/*
+ * This interprets names like ':/Initial revision of "git"' by searching
+ * through history and returning the first commit whose message starts
+ * the given regular expression.
+ *
+ * For future extension, ':/!' is reserved. If you want to match a message
+ * beginning with a '!', you have to repeat the exclamation mark.
+ */
+#define ONELINE_SEEN (1u<<20)
+
+int handle_one_ref(const char *path,
+		const unsigned char *sha1, int flag, void *cb_data);
+
+int get_sha1_oneline(const char *prefix, unsigned char *sha1,
+			    struct commit_list *list);
+
+struct grab_nth_branch_switch_cbdata {
+	long cnt, alloc;
+	struct strbuf *buf;
+};
+
+int grab_nth_branch_switch(unsigned char *osha1, unsigned char *nsha1,
+				  const char *email, unsigned long timestamp, int tz,
+				  const char *message, void *cb_data);
+
+/*
+ * Parse @{-N} syntax, return the number of characters parsed
+ * if successful; otherwise signal an error with negative value.
+ */
+int interpret_nth_prior_checkout(const char *name, struct strbuf *buf);
+
+int get_sha1_mb(const char *name, unsigned char *sha1);
+
+/*
+ * This reads short-hand syntax that not only evaluates to a commit
+ * object name, but also can act as if the end user spelled the name
+ * of the branch from the command line.
+ *
+ * - "@{-N}" finds the name of the Nth previous branch we were on, and
+ *   places the name of the branch in the given buf and returns the
+ *   number of characters parsed if successful.
+ *
+ * - "<branch>@{upstream}" finds the name of the other ref that
+ *   <branch> is configured to merge with (missing <branch> defaults
+ *   to the current branch), and places the name of the branch in the
+ *   given buf and returns the number of characters parsed if
+ *   successful.
+ *
+ * If the input is not of the accepted format, it returns a negative
+ * number to signal an error.
+ *
+ * If the input was ok but there are not N branch switches in the
+ * reflog, it returns 0.
+ */
+int interpret_branch_name(const char *name, struct strbuf *buf);
+
+int strbuf_branchname(struct strbuf *sb, const char *name);
+
+int strbuf_check_branch_ref(struct strbuf *sb, const char *name);
+
+/*
+ * This is like "get_sha1_basic()", except it allows "sha1 expressions",
+ * notably "xyz^" for "parent of xyz"
+ */
+int get_sha1(const char *name, unsigned char *sha1);
+
+/* Must be called only when object_name:filename doesn't exist. */
+void diagnose_invalid_sha1_path(const char *prefix,
+				       const char *filename,
+				       const unsigned char *tree_sha1,
+				       const char *object_name);
+
+/* Must be called only when :stage:filename doesn't exist. */
+void diagnose_invalid_index_path(int stage,
+					const char *prefix,
+					const char *filename);
+
+int get_sha1_with_mode_1(const char *name, unsigned char *sha1, unsigned *mode,
+			 int only_to_die, const char *prefix);
+
+char *resolve_relative_path(const char *rel);
+
+int get_sha1_with_context_1(const char *name, unsigned char *sha1,
+			    struct object_context *oc,
+			    int only_to_die, const char *prefix);
+
+#endif
diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
index 72934a5..fec448b 100755
--- a/t/t3404-rebase-interactive.sh
+++ b/t/t3404-rebase-interactive.sh
@@ -782,4 +782,69 @@ test_expect_success 'warn before rewriting published history' '
 	)
 '
 
+test_expect_success 'warn before rewriting published history: one user' '
+	test_when_finished "rm -rf git.git git" &&
+	git init git.git &&
+	git clone git &&
+	(
+		cd git &&
+		git config rebase.checkremoterefs true &&
+		test_commit one_commit main.txt one_commit &&
+		git push --all
+		set_copy_editor &&
+		TODO_DUMP=actual EDITOR=./editor.sh git commit --amend &&
+		tmp=$(cat actual | sed -n 1p) &&
+		echo "$tmp" >actual &&
+		echo "# The commit to reword is already published." >expected &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'warn before rewriting published history: several users' '
+	test_when_finished "rm -rf git.git git1 git2" &&
+	git init --bare --share git.git &&
+	git clone git.git git1 &&
+	git clone git.git git2 &&
+	(
+		cd git1 &&
+		test_commit one_commit main.txt one_commit &&
+		git push --all
+	) &&
+	(
+		cd git2 &&
+		git pull &&
+		test_commit two_commit main.txt two_commit &&
+		git push --all
+	) &&
+	(
+		cd git1 &&
+		git config rebase.checkremoterefs true &&
+		set_copy_editor &&
+		TODO_DUMP=actual EDITOR=./editor.sh git commit --amend &&
+		tmp=$(cat actual | sed -n 1p) &&
+		echo "$tmp" >actual &&
+		echo "# The commit to reword is already published." >expected &&
+		test_cmp expected actual
+	)
+'
+
+test_expect_success 'warn before rewriting published history: shared branch' '
+	test_when_finished "rm -rf git.git git" &&
+	git init git.git &&
+	git clone git &&
+	(
+		cd git &&
+		git config rebase.checkremoterefs true &&
+		git checkout -b sharedbranch &&
+		test_commit one_commit main.txt one_commit &&
+		git push --set-upstream origin sharedbranch &&
+		set_copy_editor &&
+		TODO_DUMP=actual EDITOR=./editor.sh git commit --amend &&
+		tmp=$(cat actual | sed -n 1p) &&
+		echo "$tmp" >actual &&
+		echo "# The commit to reword is already published." >expected &&
+		test_cmp expected actual
+	)
+'
+
 test_done
-- 
1.7.8

[PATCHv3 1/2] Warnings before rebasing -i published history

From: Lucien Kong <hidden>
Date: 2016-06-15 22:54:02

"git rebase -i" can be very dangerous if used on an already published
history. This code detects that one is rewriting a commit that is an
ancestor of a remote-tracking branch, and warns the user through the
editor. This feature is controlled by a new config key
rebase.checkremoterefs.

Here is an example of the behaviour of this feature:

   pick <id> commit pushed_one
   # Commits above this line appear in origin/master
   pick <id> commit pushed_two
   pick <id> commit pushed_three
   # Commits above this line appear in origin/next
   pick <id> commit local

Signed-off-by: Lucien Kong <redacted>
Signed-off-by: Valentin Duperray <redacted>
Signed-off-by: Franck Jonas <redacted>
Signed-off-by: Thomas Nguy <redacted>
Signed-off-by: Huynh Khoi Nguyen Nguyen <redacted>
Signed-off-by: Matthieu Moy <redacted>
---
The remote points are now only shown once. The behavior of
git branch --contains is the same than in v2 (not optimized).

 Documentation/config.txt      |    5 +++++
 Documentation/git-rebase.txt  |    5 +++++
 git-rebase--interactive.sh    |   32 ++++++++++++++++++++++++++++++++
 git-rebase.sh                 |    2 ++
 t/lib-rebase.sh               |   11 +++++++++++
 t/t3404-rebase-interactive.sh |   27 +++++++++++++++++++++++++++
 6 files changed, 82 insertions(+), 0 deletions(-)
diff --git a/Documentation/config.txt b/Documentation/config.txt
index 915cb5a..e47f6e0 100644
--- a/Documentation/config.txt
+++ b/Documentation/config.txt
@@ -1753,6 +1753,11 @@ rebase.stat::
 rebase.autosquash::
 	If set to true enable '--autosquash' option by default.
 
+rebase.checkremoterefs::
+	If it is set to 'true', git rebase -i will show after each
+	commit ancestor of a remote-tracking branch the name of these
+	branches through the editor.
+
 receive.autogc::
 	By default, git-receive-pack will run "git-gc --auto" after
 	receiving data from git-push and updating refs.  You can stop
diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index 147fa1a..b68a80b 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -208,6 +208,11 @@ rebase.stat::
 rebase.autosquash::
 	If set to true enable '--autosquash' option by default.
 
+rebase.checkremoterefs::
+	If it is set to 'true', git rebase -i will show after each
+	commit ancestor of a remote-tracking branch the name of these
+	branches through the editor.
+
 OPTIONS
 -------
 <newbase>::
diff --git a/git-rebase--interactive.sh b/git-rebase--interactive.sh
index 0c19b7c..7e4ae16 100644
--- a/git-rebase--interactive.sh
+++ b/git-rebase--interactive.sh
@@ -684,6 +684,37 @@ rearrange_squash () {
 	rm -f "$1.sq" "$1.rearranged"
 }
 
+# Add the name of the branches after each pick, fixup or squash commit that
+# is an ancestor of a remote-tracking branch.
+add_remoterefs () {
+	to_print=
+	branches_name=
+	while read -r command sha1 message
+	do
+		git branch -r --contains "$sha1" >"$1.branch"
+		if test -s "$1.branch"
+		then
+			while read -r branch
+			do
+				branches_name="$branches_name $branch"
+			done <"$1.branch"
+		fi
+
+		if test "$to_print" != "$branches_name"
+		then
+			if test -n "$to_print"
+			then
+				printf '%s\n' "# Commits above this line appear in:$to_print"
+			fi
+			to_print=$branches_name
+		fi
+		branches_name=
+		printf '%s\n' "$command $sha1 $message"
+	done >"$1.published" <"$1"
+	cat "$1.published" >"$1"
+	rm -f "$1.published" "$1.branch"
+}
+
 case "$action" in
 continue)
 	# do we have anything to commit?
@@ -857,6 +888,7 @@ fi
 
 test -s "$todo" || echo noop >> "$todo"
 test -n "$autosquash" && rearrange_squash "$todo"
+test -n "$checkremoterefs" && add_remoterefs "$todo"
 cat >> "$todo" << EOF
 
 # Rebase $shortrevisions onto $shortonto
diff --git a/git-rebase.sh b/git-rebase.sh
index e616737..f8675b5 100755
--- a/git-rebase.sh
+++ b/git-rebase.sh
@@ -99,7 +99,9 @@ action=
 preserve_merges=
 autosquash=
 keep_empty=
+checkremoterefs=
 test "$(git config --bool rebase.autosquash)" = "true" && autosquash=t
+test "$(git config --bool rebase.checkremoterefs)" = "true" && checkremoterefs=t
 
 read_basic_state () {
 	head_name=$(cat "$state_dir"/head-name) &&
diff --git a/t/lib-rebase.sh b/t/lib-rebase.sh
index 6ccf797..4906867 100644
--- a/t/lib-rebase.sh
+++ b/t/lib-rebase.sh
@@ -65,3 +65,14 @@ EOF
 	test_set_editor "$(pwd)/fake-editor.sh"
 	chmod a+x fake-editor.sh
 }
+
+
+set_copy_editor () {
+	echo "#!$SHELL_PATH" >editor.sh
+	cat >> editor.sh <<\EOF
+cat "$1" >"$TODO_DUMP"
+EOF
+
+	test_set_editor "$(pwd)/editor.sh"
+	chmod a+x editor.sh
+}
diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh
index 025c1c6..72934a5 100755
--- a/t/t3404-rebase-interactive.sh
+++ b/t/t3404-rebase-interactive.sh
@@ -755,4 +755,31 @@ test_expect_success 'rebase-i history with funny messages' '
 	test_cmp expect actual
 '
 
+test_expect_success 'warn before rewriting published history' '
+	test_when_finished "rm -rf git.git git" &&
+	git init git.git &&
+	git clone git &&
+	(
+		cd git &&
+		git config rebase.checkremoterefs true &&
+		test_commit one_commit main.txt one_commit &&
+		test_commit two_commit main.txt two_commit &&
+		test_commit three_commit main.txt three_commit &&
+		git push --all &&
+		test_commit four_commit main.txt four_commit &&
+		set_copy_editor &&
+		TODO_DUMP=actual EDITOR=./editor.sh git rebase -i HEAD~3 &&
+		tmp=$(cat actual | sed -n 1,5p) &&
+		echo "$tmp" >actual &&
+		two_sha1=$(git rev-parse --short HEAD~2) &&
+		three_sha1=$(git rev-parse --short HEAD~1) &&
+		four_sha1=$(git rev-parse --short HEAD) &&
+		echo "pick $two_sha1 two_commit" >expected &&
+		echo "pick $three_sha1 three_commit" >>expected &&
+		echo "# Commits above this line appear in: origin/master" >>expected &&
+		echo "pick $four_sha1 four_commit" >>expected &&
+		test_cmp expected actual
+	)
+'
+
 test_done
-- 
1.7.8

Re: [PATCHv3 1/2] Warnings before rebasing -i published history

From: Nguy Thomas <hidden>
Date: 2016-06-15 22:54:03

Le 11/06/2012 23:56, Lucien Kong a écrit :
+# Add the name of the branches after each pick, fixup or squash commit that
+# is an ancestor of a remote-tracking branch.
+add_remoterefs () {
+	to_print=
+	branches_name=
+	while read -r command sha1 message
+	do
+		git branch -r --contains "$sha1">"$1.branch"
+		if test -s "$1.branch"
+		then
+			while read -r branch
+			do
+				branches_name="$branches_name $branch"
+			done<"$1.branch"
+		fi
+
+		if test "$to_print" != "$branches_name"
+		then
+			if test -n "$to_print"
+			then
+				printf '%s\n' "# Commits above this line appear in:$to_print"
+			fi
+			to_print=$branches_name
+		fi
+		branches_name=
+		printf '%s\n' "$command $sha1 $message"
+	done>"$1.published"<"$1"
+	cat "$1.published">"$1"
+	rm -f "$1.published" "$1.branch"
+}
Hum, this function doesn't consider the last commit.
It will be fixed.
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help