web-dev-qa-db-ja.com

本当に、GitでのマージがSVNよりも簡単であるという具体的な例は?

Stack Overflowの質問GitでのマージがSVNよりも優れている理由および/または理由はいくつかの素晴らしい質問です素晴らしい答え。ただし、Gitでのマージが [〜#〜] svn [〜#〜] よりもうまく機能する簡単な例を示しているものはありません。

この質問が重複してクローズされる可能性がありますが、

  1. 具体的なマージシナリオ
  2. SVNではどのように難しいですか?
  3. 同じマージがGitでどのように簡単ですか?

いくつかのポイント:

  • DVCSが何であるかについての哲学や深い説明はありません。これらは本当に素晴らしいですが、私は彼らの詳細がこれ(IMHO)の重要な答えを難読化することを望んでいません
  • 現時点では「歴史的なSVN」は気にしません。最新のGit(1.7.5)と最新のSVN(1.6.15)を比較してください。
  • 名前を変更しないでください-Gitは名前の変更と移動を検出しますが、SVNは検出しません。これは素晴らしいことですが、私はもっと深いものを探しています。名前の変更や移動を伴わない例です。
  • リベースやその他の「高度な」Git操作はありません。マージを見せてください。
58
ripper234

実用的の観点から、私が「ビッグバンマージ」問題と呼んでいるため、マージは伝統的に「難しい」ものでした。開発者がしばらくの間コードを処理していて、まだコミットしていないとします(おそらく、開発者はtrunkに対してSubversionで作業することに慣れていて、未完成のコードをコミットしません)。開発者が最終的にコミットすると、多くの変更がすべて1つのコミットにまとめられます。自分の作業をこの「ビッグバン」コミットとマージしたい他の開発者にとって、VCSツールは、最初の開発者がコミットしたポイントに到達した方法について十分な情報を持っていないので、「ここに巨大な競合がありますこの関数全体で、修正してください。」.

一方、Gitや安価なローカルブランチを持つ他のDVCSを使用する通常のstyleは、定期的にコミットすることです。かなり意味のある作業を1つ行ったら、それをコミットします。完璧である必要はありませんが、一貫性のある作業単位である必要があります。マージに戻ったとき、元の状態から現在の状態に戻ったことを示す、より小さなコミットの履歴がまだありますhow。 DVCSがこれを他の作業とマージしようとすると、いつどのような変更が行われたかについてより多くの情報が得られ、最終的に小さいおよび少ないの競合が発生します。

重要なのは、何かを終えた後でのみ1つのビッグバンをコミットすることで、Gitとの難しい問題をマージすることができるということです。 Gitは、コミットを小さくすることをお勧めします(可能な限り痛みを伴わないようにすることで)。これにより、将来のマージが容易になります。

22
Greg Hewgill

GitがSubversionよりも優れていなかったという小さな実験についてのみお伝えできます(同じ問題)。

このケースについて疑問に思っていました。同じコミットに基づく2つのブランチ「mytest1」と「mytest2」から始めます。関数blub()を含むCファイルがあります。ブランチmytest1で、「blub()」をファイル内の別の位置に移動してコミットします。ブランチmytest2で、blub()を変更してコミットします。ブランチmytest2で、「gitmergemytest1」を使用しようとします。

マージの競合が発生しているようです。 Gitが「blub()」がmytest1で移動されたことを認識し、mytest2の変更をmytest1の移動と自動マージできることを期待していました。しかし、少なくとも私が試したとき、これは自動的に機能しませんでした...

したがって、Gitは何がマージされ、何がまだマージされていないかを追跡するのにはるかに優れていることを完全に理解していますが、GitがSVNよりも優れている「純粋な」マージケースがあるのではないかと思います...

この質問は長い間私を悩ませてきたので、Gitがより良いであるのに対し、SVNでのマージは失敗する具体的な例を作成しようとしていました。

ここで見つけました https://stackoverflow.com/a/2486662/191752 ですが、これには名前の変更が含まれており、ここでの質問は名前の変更がない場合でした。

それで、これは基本的にこれを試みるSVNの例です:

bob        +-----r3----r5---r6---+
          /                /      \
anna     /  +-r2----r4----+--+     \
        /  /                  \     \
trunk  r1-+-------------------r7-- Conflict

ここでのアイデアは次のとおりです。

  • AnnaとBobはどちらも、独自のブランチ(r2、r3で作成)を持つ開発者です。
  • アンナはいくつかの変更を行います(r4)、
  • ボブはいくつかの変更を行います(r5)。
  • ボブは、アンナからの変更を自分のブランチにマージします。これにより競合が発生し、Bobが修正してからコミットします(r6)。
  • Annasの変更は、トランク(r7)にマージされます。
  • ボブは自分の変更をトランクにマージして戻そうとしますが、これも競合を引き起こします。

これが Bash スクリプトで、この競合が発生します(SVN1.6.17およびSVN1.7.9を使用)。

#!/bin/bash
cd /tmp
rm -rf rep2 wk2
svnadmin create rep2
svn co file:///tmp/rep2 wk2
cd wk2
mkdir trunk
mkdir branches
echo -e "A\nA\nB\nB" > trunk/f.txt
svn add trunk branches
svn commit -m "Initial file"
svn copy ^/trunk ^/branches/anna -m "Created branch anna"
svn copy ^/trunk ^/branches/bob  -m "Created branch bob"
svn up 
echo -e "A\nMA\nA\nB\nB" > branches/anna/f.txt
svn commit -m "anna added text"
echo -e "A\nMB\nA\nB\nMB\nB" > branches/bob/f.txt
svn commit -m "bob added text"
svn up
svn merge --accept postpone ^/branches/anna branches/bob
echo -e "A\nMAB\nA\nB\nMB\nB" > branches/bob/f.txt
svn resolved branches/bob/f.txt
svn commit -m "anna merged into bob with conflict"
svn up
svn merge --reintegrate ^/branches/anna trunk
svn commit -m "anna reintegrated into trunk"
svn up
svn merge --reintegrate --dry-run ^/branches/bob trunk

最後の「--dry-run」は、競合が発生することを示しています。代わりに、最初にアンナの再統合をボブのブランチにマージしようとすると、競合も発生します。したがって、最後のsvn mergeを次のように置き換えると

svn merge ^/trunk branches/bob

これも競合を示しています。

これはGit1.7.9.5と同じです:

#!/bin/bash
cd /tmp
rm -rf rep2
mkdir rep2
cd rep2
git init .
echo -e "A\nA\nB\nB" > f.txt
git add f.txt
git commit -m "Initial file"
git branch anna
git branch bob
git checkout anna
echo -e "A\nMA\nA\nB\nB" > f.txt
git commit -a -m "anna added text"
git checkout bob
echo -e "A\nMB\nA\nB\nMB\nB" > f.txt
git commit -a -m "bob added text"
git merge anna
echo -e "A\nMAB\nA\nB\nMB\nB" > f.txt
git commit -a -m "anna merged into bob with conflict"
git checkout master
git merge anna
git merge bob

F.txtの内容はこのように変わります。

初期バージョン

A
A
B
B

アンナの修正

A
MA
A
B
B

ボブの修正

A
MB
A
B
MB
B

アンナの支店がボブの支店に統合された後

A
MAB
A
B
MB
B

すでに多くの人が指摘しているように、問題は、SubversionがBobがすでに競合を解決したことを思い出せないことです。したがって、ボブのブランチをトランクにマージしようとすると、競合を再解決する必要があります。

Gitの動作はまったく異なります。ここにgitが行っていることのいくつかのグラフィック表現があります

bob         +--s1----s3------s4---+
           /                /      \
anna      /  +-s1----s2----+--+     \
         /  /                  \     \
master  s1-+-------------------s2----s4

s1/s2/s3/s4は、gitが取得する作業ディレクトリのスナップショットです。

ノート:

  • Annaとbobが開発ブランチを作成するとき、これは[〜#〜] not [〜#〜]gitの下でコミットを作成します。 gitは、両方のブランチが最初はマスターブランチと同じコミットオブジェクトを参照していることを覚えています。 (このコミットは、s1スナップショットを参照します)。
  • アンナが変更を実装すると、新しいスナップショット「s2」とコミットオブジェクトが作成されます。コミットオブジェクトには次のものが含まれます:
    • スナップショットへの参照(ここではs2)
    • コミットメッセージ
    • 祖先(その他のコミットオブジェクト)に関する情報
  • Bobが変更を実装すると、別のスナップショットs3 +コミットオブジェクトが作成されます
  • Bobがannasの変更を開発ブランチにマージすると、さらに別のスナップショットs4(彼の変更とannaの変更のマージを含む)+さらに別のコミットオブジェクトが作成されます。
  • アンナが変更をマスターブランチにマージして戻すと、その間にマスターが変更されていないため、これは示されている例では「早送り」マージになります。ここでの「早送り」とは、マスターが何もマージせずに、単にannaからのs2スナップショットを指すことを意味します。このような「早送り」では、別のコミットオブジェクトすらありません。 「master」ブランチは、「anna」ブランチからの最後のコミットを直接参照するようになります。
  • ボブが変更をトランクにマージすると、次のようになります。
    • gitは、s2スナップショットを作成したannaからのコミットが、s4スナップショットを作成したbobs commitの(直接の)祖先であることを検出します。
    • このため、gitは再びマスターブランチを「bob」ブランチの最後のコミットに「早送り」します。
    • 繰り返しますが、これは新しいコミットオブジェクトを作成することすらありません。 「master」ブランチは、単に「bob」ブランチの最後のコミットを指します。

これがすべてを示す「gitref-log」の出力です。

88807ab HEAD@{0}: merge bob: Fast-forward
346ce9f HEAD@{1}: merge anna: Fast-forward
15e91e2 HEAD@{2}: checkout: moving from bob to master
88807ab HEAD@{3}: commit (merge): anna merged into bob with conflict
83db5d7 HEAD@{4}: commit: bob added text
15e91e2 HEAD@{5}: checkout: moving from anna to bob
346ce9f HEAD@{6}: commit: anna added text
15e91e2 HEAD@{7}: checkout: moving from master to anna
15e91e2 HEAD@{8}: commit (initial): Initial file

あなたがこれから見ることができるように:

  • annaの開発ブランチ(HEAD @ {7})に行くとき、私たちは別のコミットに変更せず、コミットを保持します。 gitは、私たちが今別のブランチにいることを覚えています
  • HEAD @ {5}で、bobの最初のブランチに移動します。 bobはまだ何も変更していないため、これにより作業コピーがマスターブランチと同じ状態に移動します。
  • HEAD @ {2}でマスターブランチに戻るので、同じコミットオブジェクトにすべてが始まりました。
  • Head @ {1}、HEAD @ {0}は、新しいコミットオブジェクトを作成しない「早送り」マージを示しています。

「gitcat-fileHEAD @ {8} -p」を使用すると、最初のコミットオブジェクトの完全な詳細を調べることができます。上記の例では、次のようになりました。

tree b634f7c9c819bb524524bcada067a22d1c33737f
author Ingo <***> 1475066831 +0200
committer Ingo <***> 1475066831 +0200

Initial file

「ツリー」行は、このコミットが参照するスナップショットs1(== b634f7c9c819bb524524bcada067a22d1c33737f)を識別します。

「gitcat-fileHEAD @ {3} -p」を実行すると、次のようになります。

tree f8e16dfd2deb7b99e6c8c12d9fe39eda5fe677a3
parent 83db5d741678908d76dabb5fbb0100fb81484302
parent 346ce9fe2b613c8a41c47117b6f4e5a791555710
author Ingo <***> 1475066831 +0200
committer Ingo <***> 1475066831 +0200

anna merged into bob with conflict

上記は、annaの開発ブランチをマージするときに作成されるコミットオブジェクトbobを示しています。ここでも、「ツリー」行は作成されたスナップショット(ここではs3)を指します。さらに、「親」行に注意してください。 「親346ce9f」で始まる2番目のものは、後でgitに、bobの開発ブランチをmasterブランチにマージしようとすると、bobのこの最後のコミットには祖先としてのannaの最後のコミットがあることを伝えます。これが、Gitがbobの開発ブランチのマスターブランチへのマージが「早送り」であることを知っている理由です。

16
Ingo Blackman

具体的な例はありませんが、繰り返しマージは特に難しいです。呼び出された十字マージ

   a
  / \
 b1  c1
 |\ /|
 | X |
 |/ \|
 b2  c2

b2とc2のマージ


の違いを説明するSubversionWikiのwikiページ マージ情報 ベースの非対称Subversionマージ(「sync」および「reintegrate」方向)およびマージtrackingベースの対称DVCSでのマージには、「 Criss-Crossマージとの対称マージ "」セクションがあります。

6
Jakub Narębski

私が考えることができる最も具体的な例は、マージの競合を引き起こさない最も単純なマージです。ただし、その例の(TL; DR)Gitは、Subversionよりも本質的に単純な手順です。理由を確認しましょう:

Subversion

Subversionで次のシナリオを考えてみましょう。トランクと機能ブランチ:

   1  2  3
…--o--o--o              trunk
          \4  5
           o--o         branches/feature_1

マージするには、Subversionで次のコマンドを使用できます。

# thank goodness for the addition of the --reintegrate flag in SVN 1.5, eh?
svn merge --reintegrate central/repo/path/to/branches/feature_1

# build, test, and then... commit the merge
svn commit -m "Merged feature_1 into trunk!"

Subversionでは、変更をマージするには別のコミットが必要です。これは、機能ブランチ仮想ディレクトリの変更をトランクに適用してマージが行った変更を公開するためです。これにより、トランクを操作するすべての人がトランクを使用できるようになり、リビジョングラフは次のようになります。

   1  2  3      6
…--o--o--o------o       /trunk
          \4  5/
           o--o         /branches/feature_1

これがgitでどのように行われるかを見てみましょう。

Git

Gitでは、ブランチはリビジョングラフ上の栄光のブックマークであるため、このマージコミットは実際には必要ありません。したがって、同じ種類のリビジョングラフ構造では、次のようになります。

         v-- master, HEAD

   1  2  3
…--o--o--o
          \4  5
           o--o

              ^-- feature_branch

現在マスターブランチにあるヘッドを使用して、機能ブランチとの単純なマージを実行できます。

# Attempt a merge
git merge feature_branch

# build, test, and then... I am done with the merge

...そして、機能ブランチが指しているコミットにブランチを早送りします。これが可能になるのは、Gitがマージの目標が直接の子孫であり、現在のブランチが発生したすべての変更を取り込むだけでよいことを知っているためです。改訂グラフは次のようになります。

   1  2  3  4  5
…--o--o--o--o--o
               ^-- feature_branch, master, HEAD

Gitが行ったのはブランチ参照をさらに前に移動することだけなので、変更には新しいコミットは必要ありません。残っているのは、もしあれば、これを公開リポジトリに公開することだけです。

# build, test, and then... just publish it
git Push

結論

この単純なシナリオを考えると、SubversionとGitの違いで2つのことを主張できます。

  • SVNでは、マージを完了するために少なくともいくつかのコマンドが必要であり、forcesマージをコミットとして公開する必要があります。
  • Gitは1つのコマンドのみを必要とし、強制しないマージを公開する必要があります。

これが最も単純なマージシナリオであることを考えると、Subversionがgitよりも簡単であると主張するのは困難です。

より難しいマージシナリオでは、gitは、履歴の書き換えを犠牲にして、より単純なリビジョングラフを生成する ブランチをリベースする 機能も提供します。ただし、コツをつかんで、すでに公開されているものの履歴の書き換えを公開しないようにしたら、それはそれほど悪いことではありません。

インタラクティブリベース 質問の範囲外ですが、正直に言うと;これにより、コミットを再配置、スキッシュ、および削除することができます。履歴の書き換えは設計上不可能であるため、Subversionに切り替えたくありません。

4
Spoike

GitでのマージはSVNよりも簡単であるというのは神話のようです...

たとえば、Gitは、SVNとは異なり、変更のある作業ツリーにマージできません。

次の単純なシナリオを考えてみましょう:作業ツリーにいくつかの変更があり、独自の変更をコミットせずにリモート変更を統合したい

[〜#〜] svn [〜#〜]update、[競合の解決]。

Gitstashfetchrebasestash pop、[競合の解決]、[stash drop競合があった場合]。

または、Gitでもっと簡単な方法を知っていますか?


ところで、このユースケースは非常に重要であるため、IntelliJは、上記の手動手順を自動化できるGitの欠落している「プロジェクトの更新」機能(SVN更新へのアナログ)を実装しました。

IntelliJ IDEA - Update Project

2
Eugen Labun

答えを短くする-DVCSでは、ローカルソース管理があるため、マージプロセスで何かが失敗した場合(大規模なマージで発生する可能性があります)、変更が加えられた以前のローカルバージョンにいつでもロールバックできます。マージする前に作成してから、再試行してください。

したがって、基本的には、プロセス中にローカルの変更が破損することを恐れずにマージを実行できます。

2
TheGreyMatter

「Merge-RefactoredHell」を除外すると、サンプルを取得できませんfairサンプルが存在しないため、

1
Lazy Badger