假设我有一个分支b
跟踪本地分支master
。
我正在尝试编写一个脚本,以将b
的所有提交作为一个单元在当前master
指向的任何树的顶部上进行选择。
因为这是非交互式脚本的一部分,所以重要的一点是,选择成功必须始终成功,而绝不能退回到交互式输入。
是否存在可以用于指导git直接提交合并冲突的合并策略或标志的某种组合?
我可以在删除合并冲突之后修改提交。
主要目的是学习如何编写git脚本,而仅部分实现自动化当前工作流程的一部分。我知道,不断挑剔不是The Git Way,而是我抛弃了本地开发历史。使用大量相互跟踪的本地分支机构也不是全部可行的方法。
出于这个问题的目的,请考虑一下本地存储库中的历史记录,从外部世界来看,它们整洁有序比准确的本地历史记录更重要>。
所以,这是我要解决的情况的一个示例。
创建沙箱目录
$ mkdir -p /tmp/gitdir
导航到沙箱目录
$ cd /tmp/gitdir
创建git repo和master分支
$ git init
写入文件,添加到git中,提交。
$ echo master > foo.txt`
$ git add foo.txt`
$ git commit -m 'user commit 1'`
[master (root-commit) e9bcb91] user commit 1
1 file changed, 1 insertion(+)
create mode 100644 foo.txt
创建新分支b
$ git checkout -b b
Switched to a new branch 'b'
更改foo.txt
的内容并提交
$ echo b1 > foo.txt
$ git add -u
$ git commit -m 'user commit 2'
设置b
来跟踪母版
$ git branch -u master
创建分支c
$ git checkout -b c
跟踪b
中的c
$ git branch -u b
向分支c
添加2次提交
$ echo c1 > foo.txt
$ git add -u
$ git commit -m 'user commit 3'
[c 04da4ab] user commit 3
1 file changed, 1 insertion(+), 1 deletion(-)
$ echo c2 > foo.txt
$ git add -u > foo.txt
$ git commit -m 'user commit 4'
[c 17df476] user commit 4
1 file changed, 1 insertion(+), 1 deletion(-)
返回b
,并添加一个提交。
$ git checkout b
Switched to branch 'b'
Your branch is ahead of 'master' by 1 commit.
(use "git push" to publish your local commits)
$ echo b2 > foo.txt
$ git add -u
$ git commit -m 'user commit 5'
[b 30f68fa] user commit 5
1 file changed, 1 insertion(+), 1 deletion(-)
返回分支c
。
$ git checkout c
Switched to branch 'c'
Your branch and 'b' have diverged,
and have 2 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
因此,关于如何解决这种情况,我们有两种选择。
大多数情况下,我想在这种情况下要做的就是将更改移至另一个分支中的所有更改之后。
在这种情况下,rebase
在大多数情况下是正确的,但有时会导致过时的提交。我真正想做的是将分支的内容向前移动,在图中将其视为 patch 或 delta 。
这是我尝试编写一个脚本来自动在正在跟踪的分支之上自动挑选分支的内容。
当前的问题是git cherry-pick
子进程有时由于合并冲突而放弃,我希望它仅提交冲突的文件。
请考虑将此脚本作为一种工作量证明。尽管对脚本本身的反馈很感激,但这并不是问题的重点。该脚本在这里主要是我要做什么以及为什么的具体“证据”。
#!/usr/bin/env perl
use strict;
use warnings;
use Carp;
use Data::Dumper;
use vars qw[*CHERRY_PICK_SINK];
BEGIN {
$Carp::Verbose = 1;
}
# accepts: command string default command interpreter
# returns: lines of output with $/ stripped, error status
sub capture_lines {
local ${^CHILD_ERROR_NATIVE};
my ($cmd) = @_;
croak if ref $cmd;
my @o = `$cmd`;
chomp foreach @o;
return [@o], ${^CHILD_ERROR_NATIVE};
}
# accepts: ()
# returns: UUID, error
sub get_uuid {
my $err;
my $cmd = q[python -c 'import uuid; print(str(uuid.uuid4()))'];
my $lines;
($lines, $err) = capture_lines($cmd);
return undef, $err if $err;
if (@$lines <= 0) {
return [undef, 'empty output'];
}
my $line = $lines->[0];
return $line, undef;
}
# accepts: ()
# returns: aref of hashes for current branch, error status
sub current_branch_hashes {
my $cmd = q[git log --format="%H" '@{upstream}..HEAD'];
my ($name, $err) = capture_lines($cmd);
return $name, $err;
}
# accepts: ()
# returns: name of current branch
sub current_branch_name {
my $cmd = q[git rev-parse --abbrev-ref --symbolic-full-name HEAD];
my ($lines, $err) = capture_lines($cmd);
my $name = $lines->[0];
return $name, $err;
}
# accepts: ()
# returns: name of upstream, error status
sub current_branch_upstream_name {
my $cmd = q[git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}'];
my ($lines, $err) = capture_lines($cmd);
my $name = $lines->[0];
return $name, $err;
}
# accepts: committish (be careful)
# returns: hash, error code
sub rev_parse {
my ($name) = @_;
croak if ref $name;
my $name_quoted = quotemeta($name);
my $cmd = "git rev-parse ${name_quoted}";
my ($lines, $err) = capture_lines($cmd);
return $lines->[0], $err;
}
# accepts: branch_name, committish
# returns: error code
sub assign_branch {
my ($key, $value) = @_;
croak if ref $key;
croak if ref $value;
my $key_quoted = quotemeta($key);
my $value_quoted = quotemeta($value);
my $cmd = "git branch -f $key_quoted $value_quoted";
my (undef, $err) = capture_lines($cmd);
return $err;
}
# accepts: branch_name
# returns: error code
sub delete_branch {
my ($key) = @_;
croak if ref $key;
my $key_quoted = quotemeta($key);
my $cmd = "git branch -D ${key_quoted}";
my $err;
(undef, $err) = capture_lines($cmd);
return $err;
}
# accepts: label1, label2
# returns: error status
# note: swaps the where the branch labels point to
sub swap_branch_labels {
my ($label1, $label2) = @_;
croak if ref $label1;
croak if ref $label2;
my ($hash1, $hash2, $err);
($hash1, $err) = rev_parse($label1);
return $err if $err;
($hash2, $err) = rev_parse($label2);
return $err if $err;
$err = assign_branch($label1, $hash2);
return $err if $err;
$err = assign_branch($label2, $hash1);
return $err if $err;
}
# accepts: committish
# returns: error status
sub checkout_old {
my ($name) = @_;
my $name_quoted = quotemeta($name);
my $cmd = "git checkout ${name_quoted}";
(undef, my $err) = capture_lines($cmd);
return $err;
}
# accepts: name
# returns: error status
sub checkout_new {
my ($name) = @_;
my $name_quoted = quotemeta($name);
my $cmd = "git checkout -b ${name_quoted}";
(undef, my $err) = capture_lines($cmd);
return $err;
}
# accepts: aref of commit hashes
# returns: exit status
sub cherrypick_aref {
local *CHERRY_PICK_SINK;
local ${^CHILD_ERROR_NATIVE};
my ($hashes) = @_;
my $cmd = 'git cherry-pick --stdin';
open CHERRY_PICK_SINK, '|-', $cmd;
for my $item (@$hashes) {
chomp($item);
print CHERRY_PICK_SINK "$item\n";
}
close CHERRY_PICK_SINK;
return ${^CHILD_ERROR_NATIVE};
}
# accepts: ()
# returns: error
sub cherrypick_self {
my ($hashes, $err) = current_branch_hashes();
return "current_branch_hashes: $err" if $err;
return "cherrypick_self: empty hashes" unless @$hashes >= 1;
my $current_branch;
($current_branch, $err) = current_branch_name();
return "current_branch_name: $err" if $err;
my $temp_branch;
($temp_branch, $err) = get_uuid();
return "get_uuid: $err" if $err;
my $upstream;
($upstream, $err) = current_branch_upstream_name();
return "current_branch_upstream_name: $err" if $err;
$err = checkout_old($upstream);
return "checkout_old: $err" if $err;
$err = checkout_new($temp_branch);
return "checkout_new: $err" if $err;
$err = cherrypick_aref($hashes);
return "cherry-pick: $err" if $err;
$err = swap_branch_labels($temp_branch, $current_branch);
return "swap branch labels: $err" if $err;
$err = delete_branch($temp_branch);
return "delete branch: $err" if $err;
}
cherrypick_self();
答案 0 :(得分:2)
是否存在可以用于指导git直接提交合并冲突的合并策略或标志的某种组合?
不。您可以可以写一个,但这承担了很大的责任。您可以可以然后委派几乎所有的责任,但这会有些棘手。
rebase的-s
选项使用git cherry-pick
来调用合并机制,包括提供-s strategy
的选项。换句话说,您可以使用git rebase -s resolve
代替git rebase -s recursive
。 1 依次表示您可以编写自己的 own 策略并将其放置在您的$PATH
作为可执行文件,例如git-merge-gregory
。然后,运行git rebase -s gregory
会在每次提交时都调用您的git-merge-gregory
程序。
不幸的是,关于如何实际调用git-merge-strategy
的文档只有零篇。在旧的git stash
脚本中,可以看到如何直接调用它。因此,我们应该查看旧的Git(例如2.6.0)来找到these lines in its git-stash.sh
。我不会引用其中的大多数内容,但是有一些神奇的export
可以将环境变量设置为 label 提交,然后加上以下内容:
if git merge-recursive $b_tree -- $c_tree $w_tree
then
# No conflict
因此,您的git-merge-gregory
应该是一个可执行程序,该程序至少要 合并基础提交或树$b_base
的哈希ID,双破折号, “其”提交或树$c_tree
的哈希ID,以及当前提交或树$w_tree
的哈希ID。 (当然,您可能还会向用户传递其他传递给-X
命令的git rebase
参数。)
您的程序现在必须完成整个合并,并以零状态退出以表明合并成功,或者在索引和工作树中留下一个合并混乱并退出非零以指示用户应在您之后进行清理。
幸运的是,这时您可以作弊:使用所有这些参数调用git-merge-recursive
并检查其退出状态。如果退出零,则操作完成。如果退出非零值,则可以让您的程序尝试使用喜欢的任何代码来清理留下的混乱git-merge-recursive
。这可能是您进行实验的方法。
1 此特定示例毫无意义,因为rebase直接调用git-merge-strategy
,并为其提供恰好一个合并基础提交哈希ID。 -s resolve
和-s recursive
之间的区别仅在git merge
调用具有多个合并基础的策略时出现。因此,这两种行为在所有变基的樱桃选择情况下都完全相同。
重点是学习如何编写git ...
这可能是错误的任务。大多数Git脚本涉及使用各种选项运行git rev-parse
和/或使用各种选项运行git rev-list
,从中获取哈希ID,然后在这些哈希ID上运行其他Git管道命令。通常,所有这些都以简单的方式处理索引。合并既麻烦又困难,有很多极端情况,并且需要对Git索引进行特殊处理,在这种情况下,索引被扩展以容纳每个文件最多三个副本,而不是每个文件一个副本。