Как объединить два git репозитория так, чтобы некоторые папки присутствовали только в одном из них? - PullRequest
0 голосов
/ 11 апреля 2020

Я работаю над небольшим веб-приложением для организации лекционных заметок. Приложение и некоторый фиктивный предварительный контент размещены на Gitlab и доступны через Gitlab Pages. Это выглядит так:

project-name/web <- the actual code
project-name/tex <- dummy content

На моем локальном компьютере имеется соответствующий контент, а также папки с дополнительным содержимым, которые не отслеживаются и, следовательно, не присутствуют в репозитории Gitlab, поскольку это лекционные заметки, которые не должно быть общедоступным. Это выглядит так:

project-name/web
project-name/tex <- dummy and proper content
project-name/folder1 <- further content
project-name/folder2 <- further content

Теперь я хотел бы разместить приложение с правильным содержимым на моем Raspi (используя nginx). Я создал (голое) репо git на raspi, добавил полные файлы проекта, включая соответствующий контент (все папки), к этому репо и настроил хук git для его развертывания на сервере nginx, который скопируйте файлы в / var / www/html и запустите скрипт PHP, который также необходим.

Но теперь у меня есть два репозитория, Gitlab и Raspi, и мне нужно будет внести все изменения в код дважды. Я исследовал, как объединить два репозитория, и получил подсказку, что можно добавить «веб-папку», которая является общей для обоих репозиториев, как субмодуль репо Raspi, а затем внести изменения в код репозитория Gitlab и вытащить их в подмодуль репо Raspi. Но это не совсем работает, потому что «сеть» является подпапкой репозитория Gitlab, а не всего репо. Таким образом, люди указали мне на редкие коммиты, чтобы выбрать только одну подпапку, но это сохраняет структуру папок и, следовательно, также не работает должным образом.

Я не очень опытен с git, знаю только очень Мне кажется, что команды basi c и эти субмодули и разреженные коммиты довольно запутаны, и я не могу судить, подходят ли они для решения проблемы.

Я почти уверен, что мой сценарий не редкость, но мне все еще не удалось найти подходящее решение, поэтому любой намек на какое-то чтение очень важен!

1 Ответ

1 голос
/ 11 апреля 2020

Git не хранит папки.

В некотором смысле Git даже не хранит файлов . Git хранит - на том уровне, на котором вы будете его использовать, в любом случае - это большая база данных коммитов , 1 плюс меньшая база данных имен. Это коммиты, которые хранят файлы. Это может показаться огромной разницей, но это действительно разница и ключ ко всему.

Объединение двух Git репозиториев состоит из получения всех коммитов в обоих исходных репозиториях. и положить их в одну большую кучу. Построение желаемого набора имен для полученной расширенной базы данных обычно является основной проблемой, но вы пропускаете сразу же к второй проблеме вашего собственного изобретения. ? Как мы увидим в конце, это может быть совсем не то, что вам нужно.

В любом случае, первое, что вам нужно знать здесь, это что такое коммит и что он делает, так как это уровень в котором вы можете использовать Git. Давайте начнем с простого, но раздражающего факта, что каждый коммит имеет уникальный га sh ID , большую уродливую строку букв и цифр, такую ​​как 9fadedd637b312089337d73c3ed8447e9f0aa775. По сути, это и есть истинное имя коммита: Git находит объект в своей большой базе данных.

Каждый коммит хранит некоторый набор файлов: никаких папок, только файлы. Файлы, хранящиеся с фиксацией - как бы основными данными фиксации - находятся в специальном, Git -сжатом, сжатом формате только для чтения. 2 Коммиты и их файлы заморожены для всех время, поэтому, чтобы использовать их или изменить их, Git должен извлечь их (что мы получим через минуту). Они формируют моментальный снимок как бы: Ваши файлы выглядели так, когда вы делали этот коммит.

Помимо этого снимка, каждый коммит также содержит некоторые метаданные , например, кто это сделал, когда и почему. Большая часть этих метаданных предназначена для потребления человеком, но одна часть предназначена для самого Git: каждый коммит хранит список необработанных идентификаторов ha sh его непосредственных родительских коммитов. Большинство коммитов имеют ровно одного родителя. Когда у нас есть такие коммиты с одним родителем, они формируют обратную цепочку коммитов:

... <-F <-G <-H

Эта цепочка в конечном итоге заканчивается (справа, здесь) чем угодно last (самый последний) коммит был. У него большой некрасивый идентификатор ha sh, но я просто использовал букву H, чтобы заменить этот идентификатор ha sh. Фиксация находится в большой базе данных Git, которую можно получить по этому идентификатору ha sh. Внутри коммита есть идентификатор ha sh его родителя G, поэтому при заданном коммите H, Git можно найти и получить G. G, конечно, имеет родителя F, поэтому теперь Git может получить F, у которого есть родитель, и так далее. Это возвращается во времени, в конечном итоге к самому первому коммиту, который, будучи первым, просто не имеет родителя.

A имя ветви просто содержит (одиночный) идентификатор ha sh последний коммит. Таким образом, если в этом хранилище всего восемь коммитов с A по H и только одно имя ветки master, мы имеем:

A--...--G--H   <-- master

в качестве всего хранилища. Каждый из этих восьми коммитов имеет свой снимок всех файлов. Git покажет вам, что изменило , между любой парой коммитов, извлекая во временную область (в памяти) коммит и его родителя и видя, какие файлы одинаковы - о чем Git вообще ничего не скажет - а они разные. Для тех, кто отличается, Git даст вам рецепт, с помощью которого вы можете изменить предыдущий коммит, превратив его в более поздний.

Чтобы добавить новый коммит, вы:

  • Пусть Git извлекает последний коммит ветви в рабочую область: это ваше рабочее дерево или рабочее дерево . Git также помещает копии файлов в замороженном формате, сжатых и Git -тифицированных в Git index на этом этапе. 3 Этот последний коммит теперь является текущим коммитом , а используемое вами имя ветви - например, master в git checkout master - является текущей веткой .

  • Суетитесь с копиями рабочего дерева, как вам нравится.

  • Используйте git add для копирования обновленных файлов рабочего дерева обратно в Индекс Git.

  • Выполнить git commit. Он собирает некоторые метаданные от вас и ваших настроек, текущей даты и времени и т. Д .; использует текущий коммит в качестве родителя для нового коммита; использует все, что есть в индексе Git прямо сейчас, как новые файлы для всех времен и записывает новый коммит. Запись нового коммита дает ему новый уникальный идентификатор ha sh.

Git теперь сохраняет идентификатор ha sh нового коммита в текущем имени ветви . Так что когда master использовался для указания на H, теперь он указывает на новый коммит, который мы назовем I, который указывает на H:

...--G--H--I   <-- master

Так растут ветви .

Обратите внимание, что I имеет полный снимок каждого файла, как и H. Это файлы, которые вы получите в своем рабочем дереве позже, если вы проверяете коммит I.


1 Технически, это Git s база данных объектов и вы также можете напрямую взаимодействовать с объектами тегов иногда, если вы используете аннотированные теги.

2 Технически, что такое Git в коммите хранится * ha sh ID объекта дерева . Объекты дерева имеют записи, каждая запись задает имя файла или его часть, его режим и идентификатор ha sh объекта blob , в котором хранится содержимое файла. Древовидные объекты могут позволять Git хранить папки, но Git создает и использует эти древовидные объекты через Git index , который допускает только записи в файлах, так что Git не хранит папку.

3 Индекс, упомянутый в сноске 2, показывает, как Git создает следующий коммит. Он имеет несколько дополнительных применений, и мы не будем go подробно здесь. Он не хранит буквально копии файлов: он хранит режим, имя файла (полный путь, такой как path/to/file) и Git blob-object ha sh ID. На этом уровне, однако, вы можете просто думать об индексе как о сохранении копии файла в замороженном формате, готовой к go в следующем коммите.


Объединение репозиториев

Если вы хотите объединить два репозитория в один большой, вы:

  • Возможно, начните с клонирования одного из двух репозиториев, чтобы вы работали с копией на случай, если ты испортил Gets Это даст вам копию всех коммитов. Будучи клоном, эта копия имеет собственных имен ветвей: все имена ветвей оригинала были переименованы и теперь вместо них origin/master, origin/dev, et c. из master и dev и т. д.

    Процесс клонирования принимает имя - git clone -b <em>branch</em> - как имя, которое он должен создать для вас. Если вы его не дадите, он спросит origin Git, какую ветку рекомендует . Обычно он рекомендует master. Таким образом, ваш клон обычно заканчивается веткой master, которую ваш Git устанавливает для указания на такой же коммит, который ваш Git установил для origin/master, основываясь на их master .

    (Посмотрите на рисунки выше и посмотрите, как это делает ваши master равными master.)

  • Есть Git add все коммиты из второго хранилища в эту копию. Как и прежде, пусть ваши Git переименуют все их ветви. Мы посмотрим, как это работает в данный момент.

Имена ветвей и все другие записи Git name-to-ha sh -ID, составляют other база данных в Git хранилище. Выше мы видели, как имя ветви выбирает последний коммит в цепочке коммитов и как клонирование переименовывает других Git имен ветвей. Эти origin/* имена имена для удаленного слежения , 4 , которые просто запоминают , куда указывали другие ветви Git, когда я в последний раз разговаривал с этим другим Git и получил список коммитов, на которые указали имена его ветвей.

Чтобы получить коммиты от другого Git, вам нужен URL (или иногда имя пути на вашем компьютере, но мы просто притворимся, что это URL здесь). Когда вы клонируете репозиторий Git, вы даете Git URL-адрес: git clone ssh://git@github.com/<em>user</em>/<em>repo</em>, например. Ваш Git:

  1. создает новый пустой каталог (обычно - вы можете указать его на существующий пустой каталог) и входит в этот каталог для остальных шагов;
  2. git init: создает здесь новый пустой Git репозиторий;
  3. git remote add ...: добавляет удаленное имя, по умолчанию origin, для хранения URL;
  4. выполняет любую дополнительную конфигурацию, которую вы запрашиваете;
  5. запускает git fetch на новом пульте; и
  6. последний, запускает git checkout для создания и извлечения master или любого другого имени, которое вы выбрали.

На шаге 5 ваш Git вызывает другой Git , используя сохраненный URL. Другие Git передают любые коммиты, которые есть у вашего Git, а это все их коммиты, после перечисления всех их имен веток и идентификаторов чаевых коммитов ha sh (и имен тегов и других имен, но здесь мы не будем учитывать эту сложность).

Именно этот шаг копирует все их коммиты и создает или обновляет ваши имена для удаленного отслеживания. Поэтому, если мы хотим добавить все коммиты из другого Git, нам просто нужно выполнить:

git remote add <name> <url>

Вы выбираете какое-нибудь имя - second, another, что угодно вам нравится - и URL. Ваш Git добавляет новый пульт, хранящий этот URL. Затем вы можете запустить:

git fetch <name>

Это заставит ваш Git позвонить на другой Git. Они перечисляют свои имена ветвей (и другие имена, которые мы игнорируем) и последний хэш коммитов, и ваш Git запрашивает эти коммиты и каждый другой коммит, который эти коммиты имеют как родители, рекурсивно, все Вернемся к самому первому коммиту в этого хранилища.

Допустим, вы использовали имя two для второго Git. Теперь у вас есть имена для удаленного отслеживания в форме two/*, такие как two/master и two/develop и т. Д., Для поиска last коммитов в каждом из различных имен ветвей из этого Git .

Теперь вы можете сделать новые коммиты, которые объединят любые файлы из любого из этих двух репозиториев.


4 Git называет эти названия филиалов удаленного отслеживания , которые люди часто сокращают до филиалов удаленного отслеживания . Тем не менее, они вовсе не являются именами branch , в том случае, если вы дадите им git checkout или git switch, вы окажетесь в том, что Git вызывает отсоединенный HEAD режим : не на ветке. Я нахожу менее запутанным просто назвать их имена для удаленного слежения: они отслеживают имена филиалов для вас, поэтому они имена , и они выполняют функцию удаленного слежения, вот как мы должны их называть.


Interlude

Обратите внимание, что фиксирует в хранилище являются историей. История файлов отсутствует, потому что на самом деле файлов нет. Есть только коммиты, которые хранят снимки и имеют связь. Более поздние коммиты указывают на более ранние коммиты. История существует , потому что более поздние коммиты указывают на более ранние коммиты. Git может начинаться с конца и работать задом наперед, и это история.

Имена находят коммиты. Каждое имя находит один указанный c коммит. Если вы работаете задом наперед, вы получите историю. Если вы просто остаетесь там, ну, , тогда у вас есть коммит, и у коммита есть файлы, и вы можете извлекать файлы и работать с ними.

Создание комбинирующего коммита

С учетом двух советов по филиалам:

...--o--J   <-- branch1

...--o--L   <-- branch2

вы можете выбрать один из этих двух коммитов, например J, по имени его ветви - git checkout branch1 - и запустить git merge branch2.

В идеале, эти две ветви фактически начинаются с общего запуска точка: общий коммит, который находится на обеих ветвях. То есть, это действительно выглядит так:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

, где commit H является очевидным наилучшим общим общим коммитом в обеих ветвях.

The HEAD Я нарисовал здесь, как Git помнит, какое имя ветви вы сделали git checkout для: Git прикрепляет специальное имя HEAD только к одной ветви. Это тот файл, который Git извлекается из индекса Git и вашего рабочего дерева, то есть, из тех файлов, которые вы можете видеть и работать прямо сейчас из коммита J. Это одно имя, HEAD, предоставляет как имя текущей ветви , так и косвенно, по имени ветви, указывающему на фиксацию, - текущий коммит .

You Теперь запустите:

git merge branch2

и Git находит коммит L, на который branch2 указывает. Код слияния теперь работает в обратном направлении от обоих этих коммитов, J и L, чтобы найти коммит H самостоятельно. Этот коммит H является базой слияния двух ветвей.

Для выполнения sh действия слияния - слияние как глагол , как мне нравится чтобы вызвать его - Git теперь выполняет два сравнения, начиная с моментального снимка в коммите H оба раза. Команда git diff позволяет нам выполнить такое же сравнение и, следовательно, подумать о том, что Git видит:

  • git diff --find-renames <em>hash-of-H</em> <em>hash-of-J</em> находит то, что мы изменили branch1;
  • git diff --find-renames <em>hash-of-H</em> <em>hash-of-L</em> находит, что они изменились на branch2.

Действие объединения теперь объединяет два набора изменений . Что бы мы ни делали с файлом в H, Git может сделать это снова, а также добавить к нему все, что они сделали с тем же файлом в H. Выполнение этого для каждого файла и внесение любых изменений всего файла - например, добавление совершенно нового файла, если мы или они это сделали, - изменяет снимок в H в новый снимок, готовый к go.

Если все пойдет хорошо, Git теперь сделает новый коммит слияния , который мы можем нарисовать как коммит M:

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

Git корректируется имя branch1 как обычно, чтобы указать на новый коммит слияния M, который имеет снимок как обычно. Единственное, что не «как обычно», это то, что новый коммит M имеет двух родителей, J и L.

Это означает, что если мы попытаемся взглянуть на M чтобы увидеть, что изменилось, обычный трюк - сравните M с его родителем - не работает. Нет a родителя; Есть родители, множественное число. Что Git делает для этого, зависит от того, какую команду вы используете для просмотра M, но в большинстве случаев он просто сдается и вообще не показывает различий! Часто трудно увидеть прошлое слияние. Технически, слияние может иметь более двух родителей.

При прохождении истории Git обычно либо go опускается на одну "ногу" или "сторону" слияния, либо вниз на всех , Опять же, мы не будем вдаваться во все детали: все становится довольно сложно, очень быстро. Простой git log, тем не менее, go опускает обе ноги, в некотором порядке, по одному коммиту за раз.

В любом случае, реальная точка зрения здесь заключается в том, что коммит слияния M связывает два истории обратно в одну. С branch1 мы посещаем commit M; затем фиксирует J и L и I и K в некотором порядке. Обычно мы ударили по всем этим до того, как мы go вернемся к фиксации H, где все упрощается, а затем мы go продолжаем посещать фиксацию G, F и т. Д. c., Как обычно. Так все эти коммиты теперь включены branch1. Нам даже больше не нужно имя branch2: оно идентифицирует коммит L, но M достигает L, если мы go опустим его вторую ногу. Мы можем удалить имя branch2, если захотим, сейчас. 5


5 Если мы не удалим branch2, мы можем сделать больше коммитов на branch2, и они не будут на branch1. Позже мы можем снова git checkout branch1 и git merge branch2. На этот раз лучший общий коммит окажется коммитом L. Вот как работают длительные операции повторного слияния: слияния изменяют набор достижимых коммитов на одну ветвь, что делает будущие слияния в , которые работают на лучше . По крайней мере, мы надеемся, что это будет лучше: иногда это просто по-другому .


Ваш случай немного отличается

Возможно, вы на этом этапе захотите использовать:

git checkout master
git merge two/master

например, чтобы сделать комбинированный коммит. Но в современном Git вы получите ошибку:

fatal: refusing to merge unrelated histories

Проблема здесь в том, что не является общим коммитом . Старые версии Git в любом случае выполняют или, по крайней мере, пытаются объединить, используя поддельный коммит без файлов в нем: Git ' пустое дерево .

Вы можете включить это само по себе, как если бы у вас был старый Git:

git merge --allow-unrelated-histories two/master

Git, теперь будет использовать поддельный пустой коммит в качестве общей отправной точки. Каждый файл в обоих коммитах с ветвями будет «добавлен». Если все имена файлов различны, объединение выполнится само по себе, поместив все файлы в новый коммит.

Если это не нужно, то вы хотите - и это не так. 't - вы можете убедиться, что Git не не выполняет коммит самостоятельно, используя:

git merge --allow-unrelated-histories --no-commit two/master

Это гарантирует, что Git останавливается с объединение не завершено, как если бы что-то пошло не так, когда Git объединяет две фиксации самостоятельно.

Если какие-либо имена файлов сталкиваются , вы получите "add" / добавить конфликт "в любом случае, и Git остановится. Проблема в том, что Git не знает , какой файл использовать . Должен ли он использовать тот из вашего текущего коммита, который был выбран с помощью HEAD / master? Или он должен использовать тот из другого коммита, который выбран с помощью two/master?

Теперь ваша задача - предоставить правильный набор файлов для снимка слияния . Вы делаете это как в своем рабочем дереве, где вы можете видеть и работать с файлами, так и в индексе Git (который вы не очень хорошо видите: git status говорит вам, что отличается в индексе Git, а не в индексе Git, поэтому он сравнивает индексные копии файлов с другими копиями).

Возможно, вы захотите git rm или git rm --cached некоторые специфические c файлы из индекса Git (мы не будем беспокоиться об этом здесь), но в основном вы захотите исправить копии рабочего дерева, а затем просто git add копии рабочего дерева Git скопировать правильные файлы в свой индекс. При этом Git помечает каждый конфликтующий файл как разрешенный : git status удаляет их из специального (только для слияния) конфликтного раздела.

Вы должны знать, что git status сообщает вам, что будет зафиксировано («подготовлено для фиксации»):

  • , сравнивающим замороженные файлы текущего (HEAD) фиксированного файла с файлами замороженного формата в индекс
  • для каждого файла, который является одинаковым , не говоря ни слова
  • для каждого файла, различного , упомяните имя файла

, поэтому, если HEAD равно master, что также origin/master, вы можете узнать, какие это файлы, посмотрев на другой клон , который у вас есть, то есть просто ваш первый оригинальный репозиторий и посмотрите, какие файлы там извлечены.

Как только все конфликты слияния разрешены, git status также сообщает вам, что в вашем рабочем дереве отличается от того, что в Git ' индекс s. Это изменения, не подготовленные для коммита .

Чтобы завершить sh слияние и создать новый коммит слияния , который связывает две истории, теперь вам нужно только запустить:

git merge --continue

или:

git commit

(merge --continue просто проверяет, что есть слияние с fini sh, затем запускает git commit, поэтому они делают то же самое в этом случае).

Файлы, которые go в новом снимке фиксации слияния, - это файлы в индексе Git на данный момент . Таким образом, вся эта работа состоит в том, чтобы поместить нужные файлы в индекс. Вот о чем это все. Git хранит фиксирует , а не файлы; коммиты содержат файлы, как снимок, сделанный из того, что есть в индексе Git; используемые вами команды управляют индексом и создают новые коммиты.

Вам не нужно комбинировать репозитории , если вы не хотите

Если все, что вы хотите чтобы получить кучу файлов из и добавить их в новый коммит в каком-либо существующем или новом клоне, просто сделайте все возможное, чтобы получить файлы. При желании клонируйте репозиторий или переключитесь на существующий клон. Используйте любые команды, которые вам нравятся, чтобы скопировать файлы на место. Используйте git add, чтобы скопировать эти файлы в индекс Git, где они имеют пути, например folder1/file, потому что в вашем рабочем дереве есть folder1, содержащий файл с именем file.

Как только у индекса будет правильный набор файлов, запустите git commit, чтобы сделать новый коммит в текущей ветви. Git соберет метаданные, запишет новый коммит с новым снимком и сохранит идентификатор ha sh нового коммита в имени текущей ветви. Новый коммит укажет на предыдущий коммит. Вот что такое Git: добавление новых коммитов. Мы находим их по именам веток; мы сравниваем их по git diff -ing; мы делаем другие причудливые Git команды, которые делают с ними другие вещи. Но важны коммиты .

Это коммиты имеют значение

Обратите внимание, что поскольку это коммиты, которые имеют значение, вы можете, если вы хотите использовать git merge до t ie две истории вместе, не беспокоясь о снимке в слиянии. Затем вы можете сделать второй коммит, который исправляет вещи, которые были неправильны при слиянии.

Например, если Git может объединить две не связанные между собой истории самостоятельно (возможно, с * 1606) *) но это сохраняет слишком много файлов, и что? Вы можете позволить Git сделать это, затем удалить ненужные файлы и сделать второй коммит.

Git фиксирует поделиться их файлами. Каждый коммит полностью доступен только для чтения, заморожен на все времена. У вас либо есть коммит, либо его нет, и если у вас есть коммит, у него есть все его файлы. Если его файлы совпадают с файлами предыдущего коммита, Git знает, что безопасно делиться файлами в обоих коммитах. Есть только одна действительная замороженная копия.

Итак, если вы возьмете два разных репозитория и объедините их коммиты в один репозиторий, у вас уже есть все коммиты и все файлы. Создание коммита слияния, которое, если вы проверите его, принесет вам слишком много файлов, не займет дополнительного места - ну, совсем немного места для самого коммита слияния. Последующий коммит, в котором вы удаляете кучу файлов, занимает немного места для записи нового коммита, в котором говорится о повторном использовании только некоторого подмножества файлов.

Извлечение коммита, который приходит после слияние извлекает в вашу рабочую область только те файлы, которые находятся в этом коммите, так что вы не будете видеть дополнительные файлы в любом случае. Они будут в вашей истории, но они будут там, независимо от того, находятся ли они в вашем слиянии.

Выбор за вами: Git будет хранить все, что вы скажете. Вы будете иметь коммиты, которые у вас есть, какими бы они ни были, и вы не сможете изменить ни один из существующих коммитов, но вы можете выбрать, какой из них будет вашим последним коммитом. Вы даже можете создать новую историю, состоящую из одного коммита с правильными файлами:

...--W--X--Y   <-- master

 Z   <-- new-history (HEAD)

, где Z имеет нет родителя. Если вы теперь удалите все имена , которые находят все другие коммиты, такие как master:

git branch -D master

, давая:

...--W--X--Y   ??? [can't find Y any more!]

 Z   <-- new-history (HEAD)

Git в конечном итоге отбросит все остальные коммиты.

Чтобы сделать это go быстрее, git clone этот репозиторий; у вашего клона не будет origin/master, просто origin/new-history. Вы можете назвать это master теперь в новом клоне, который состоит только из одного коммита с правильными файлами. Однако его история не может быть связана с историей исходного хранилища.

Чтобы достичь этого состояния, если вы этого хотите, см. git checkout --orphan. Вы можете запустить:

git checkout master
git checkout --orphan new-history
git commit

и вы получите этот новый Z коммит с no parent, с тем же снимком, который Git имеет в качестве текущего коммита tip master , Индекс не изменился: git checkout master заполнил его, но git checkout --orphan new-history не опустошил.

Обычно это неправильно, но если вы понимаете, как и почему это работает , теперь вы получаете многое из того, о чем Git.

...