Введение в Git:
ошибки использования (ч. 9)

Введение
В предыдущей статье мы прошли базовые команды Git, которые применяются в повседневной работе. В этой публикации продолжим изучение данного инструмента, рассмотрев типичные ошибки при его использовании, а также способы их исправления.
Данная статья не ставит перед собой цель быть энциклопедией решений на все случаи жизни. Было бы неверным давать ответы на вопросы, которые на данном этапе изучения Git просто не могут возникнуть. Любая информация должна соответствовать вашему текущему уровню.
1. Самое простое решение многих проблем
Вы «натворили делов» со своим репозиторием, и вам кажется, что все сломалось и ничего не работает. В качестве максимально быстрого решения этой проблемы можно удалить локальный репозиторий (скрытую папку .git в корне вашего проекта) и репозиторий на GitHub, а затем выполнить git init. При этом ваши файлы удалять не нужно.
Когда я изучал Git, то десятки раз создавал и удалял свои репозитории, сталкиваясь с неразрешимыми для меня на тот момент проблемами.
Этот вариант имеет право на существование для начинающих разработчиков, т. к. они не обладают еще нужным набором знаний для применения более технологичных решений. Но, как вы понимаете, данный способ хорош до поры до времени.
2. Как изменить URL удаленного репозитория?
Вы неверно назвали свой репозиторий, например, вместо StartJava почему-то написали Lesson1 или JavaOps. На что наставник вам сказал, что его нужно переименовать.
Или вы удалили свой репозиторий на GitHub, а у себя на компьютере — оставили. После этого создали новый удаленный репозиторий (УР), изменив его первоначальное имя.
Например, URL репозитория выглядел так:
https://github.com/ichimax/lesson1.git
А после изменений ссылка стала следующей:
https://github.com/ichimax/startjava.git
При этом в настройках локального репозитория (ЛР) адрес старого репозитория никуда не делся — его нужно обновить.
Свяжем ЛР с новым адресом УР:
> git remote set-url origin новый_url.git
> git remote -v
Когда вам в дальнейшем потребуется выполнить push, то необходимо будет написать полную версию команды git push -u origin master, чтобы заново связать ЛР с новым УР, а затем снова можете использовать сокращенный вариант.
3. Как удалить файл из репозитория?
3.1. Удаление файла из индекса
Представим, что вы забыли занести в .gitignore правило, которое позволяло бы игнорировать class-файлы. В итоге применив команду git add, добавили их случайно в индекс.
Это очень частая ошибка у начинающих, которая влечет за собой попадание мусорных файлов в УР.
Скомпилируем исходник, создав class-файл:
> javac MyFirstApp.java
Внесем изменения в класс из предыдущих статей:
import java.util.Scanner;

public class MyFirstApp {
    public static void main(String[] args) {
        System.out.println("У какого языка программирования следующий слоган:");
        System.out.print("\"Написано однажды, ");
        System.out.println("работает везде!\"");

        String answer = new Scanner(System.in).next();
        if (answer.equalsIgnoreCase("Java")) {
            System.out.println("Вы угадали");
        } else System.out.println("Увы, но - это Java");
    }
}
Далее введем привычные команды:
> git add . && git status
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   MyFirstApp.class
        modified:   MyFirstApp.java
MyFirstApp.class в итоге попал в индекс Git. Необходимо его оттуда удалить, т. к. отслеживание изменений у данного вида файлов не несет никакой практической пользы.
Воспользуемся командой, подсказанной Git:
> git restore --staged MyFirstApp.class && git status
Changes to be committed:
        modified:   MyFirstApp.java

Untracked files:
        MyFirstApp.class
MyFirstApp.class снова стал неотслеживаемым, а измененный класс остался в индексе не тронутым.
Чтобы обезопасить себя, перед использованием команды git add, всегда нужно просматривать список файлов (git status), которые вы планируете добавить в индекс. Также обязательно добавьте шаблон *.class в .gitignore, чтобы больше не спотыкаться о class-файлы.
3.2. Удаление файла из коммита
Пойдем дальше и научимся удалять MyFirstApp.class из коммита.
Проверим состояние ЛР:
> git status

Changes to be committed:
        new file:   MyFirstApp.class
        modified:   MyFirstApp.java
Добавим все изменения в коммит:
> git commit -m "Добавил quiz по слогану Java"
Перед тем, как запушить коммит, программист решил посмотреть, какие файлы в него входят:
> git log -1 --name-only --oneline
ead5167 (HEAD -> master) Добавил quiz по слогану Java
src/MyFirstApp.class
src/MyFirstApp.java
С командной git log мы уже сталкивались в предыдущей статье. Из нового — аргумент --name-only, который позволяет вывести только имена измененных файлов, находящихся в коммите.
Использование log позволило заметить, что MyFirstApp.class оказался в коммите. Его нужно оттуда убрать.
Для этого пишем:
> git reset --soft HEAD~1
Разберем эту команду по частям:
  • reset — отменяет коммиты
  • --soft — позволяет выполнить отмену коммита без потери изменений
  • HEAD — указатель на локальную ветку в которой мы находимся. В нашем случае HEAD~1 указывает на первый коммит в дереве коммитов ветки master
Более подробно про команду reset (1, 2).
Тут следует сказать, что HEAD указывает на ветку, в которой вы находитесь в данный момент. Благодаря ему мы можем перемещаться по дереву коммитов и откатываться к любому из них.
Получается следующая картина: HEAD указывает на ветку master, а master является указателем на вершину в дереве коммитов.
Отобразим, куда указывает HEAD:
> git branch
* master
* обозначается текущая ветка.
Если команду ввести с параметром -v, то кроме названия ветки отобразится информация о коммите, на который указывает master:
> git branch -v
* master b59d871 Изменил вывод текста, отображаемого в консоль
После отмены коммита отобразим список оставшихся коммитов:
> git log --oneline
b59d871 (HEAD -> master, origin/master) Изменил вывод текста, отображаемого в консоль
39ba195 Переименовал about.txt в README.md и внес в него описание проекта
1e36e0f Инициализация проекта
Видим, что благодаря reset последний коммит из него был исключен.
При этом log отображает ту же самую информацию, о которой мы говорили выше: HEAD указывает на master, а он — на вершину дерева коммитов (коммит b59d871).
Стоит отметить, что команду reset необходимо использовать только для отмены коммитов, которые находятся исключительно на вашем компьютере и еще не попали на УР. Т. к. эта команда меняет историю коммитов, то это может принести множество проблем для других людей, которые совместно с вами работают в одном репозитории, если вы отмените с ее помощью коммит, а затем запушите изменения на УР.
Посмотрим, в каком состоянии теперь находится ЛР:
> git status
Changes to be committed:
        new file:   MyFirstApp.class
        modified:   MyFirstApp.java
Что делать дальше, вы уже знаете: нужно убрать из индекса class-файл, и закоммитить java-класс.
Исключаем MyFirstApp.class:
> git restore --staged MyFirstApp.class
> git status
Changes to be committed:
        modified:   MyFirstApp.java

Untracked files:
        MyFirstApp.class
Осталось сделать коммит.
И тут вы понимаете, что забыли, как у вас назывался отмененный коммит: изменения остались все те же, значит, придумывать описание к коммиту нет смысла — нужно воспользоваться тем, которое было раньше.
Есть одна волшебная команда git reflog, которая отобразит все, что вы когда-либо сделали в своем репозитории с коммитами. Это своего рода лог всех ваших операций. Приведу его малую часть, которая нам необходима:
> git reflog
b59d871 (HEAD -> master, origin/master) HEAD@{0}: reset: moving to HEAD~1
ead5167 HEAD@{1}: commit: Добавил quiz по слогану Java
Из вывода видно описание отмененного коммита. Используем его для нового:
> git commit -m "Добавил quiz по слогану Java"
Больше подробностей о reflog по ссылке.
Не забудьте добавить *.class в.gitignore.
3.3. Удаление файла с GitHub
Самый неприятный случай — это когда мусорный файл все же пробрался на GitHub, т. е. был запушен:
В этом случае наставник вам может написать замечание: «на GitHub не должно быть файлов с расширением *.class, только *.java. Необходимо удалить из УР все class-файлы».
Самый простой выход из этой ситуации — это удалить в ЛР class-файлы, добавить в .gitignore маску *.class и сделать новый пуш. После этих действий на GitHub class-файл удалится. Он также автоматически удалится и у всех людей, которые работают с вами в одном репозитории. Например, когда наставник перед проверкой вашего ДЗ сделает git pull, то удаленные вами class-файлы, удалятся автоматически и у него.
Выглядеть это может примерно так:
D:\Java\StartJava (master -> origin)
> git rm src\*.class
rm 'src/MyFirstApp.class'


> git status
On branch master

Changes to be committed:
        new file:   .gitignore
        deleted:    src/MyFirstApp.class

> git commit -m "Добавил .gitignore с маской *.class"
[master 4bf0ddd] Добавил .gitignore с маской *.class
 2 files changed, 1 insertion(+)
 create mode 100644 .gitignore
 delete mode 100644 src/MyFirstApp.class

> git push
4. Как изменить описание коммита?
4.1. Коммит не был запушен
Вы сделали коммит, но еще его не запушили. Выясняется, что описание к нему содержит опечатку или это сообщение нужно изменить. Разберем данную ситуацию.
Внесем ряд изменений в файл MyFirstApp.java, добавив в него код, запрашивающий у участника квиза его имя, которое будет выводиться, если он ответит правильно на вопрос:
import java.util.Scanner;

public class MyFirstApp {
    public static void main(String[] args) {
        Scanner console = new Scanner(System.in, "cp866");
        System.out.print("Введите, пожалуйста, свое имя: ");
        String name = console.next();

        System.out.println("У какого языка программирования следующий слоган:");
        System.out.print("\"Написано однажды, ");
        System.out.println("работает везде!\"");

        String answer = console.next();
        if (answer.equalsIgnoreCase("Java")) {
            System.out.println(name + ", вы угадали!");
        } else System.out.println("Увы, но - это Java");
    }
}
Выполним коммит:
> git add MyFirstApp.java
> git commit -m "Добавил ввод имени учасника"
Выясняется, что описание содержит опечатку в слове «учасника».
Для исправления воспользуемся двумя вариантами команды commit:
  • в первом случае при вводе git commit --amend, откроется редактор для изменения сообщения:
Такой способ подходит для исправления многострочных комментариев. Нам же больше подходит второй способ.
  • во втором случае команда ниже позволит внести изменение прямо из консоли:
> git commit --amend -m "Добавил ввод имени участника"
[master 96e2847] Добавил ввод имени участника
Убедимся, что описание к коммиту изменилось, а прежнее — не сохранилось, отобразив сокращенную информацию по последнему коммиту:
> git log --oneline -1
96e2847 (HEAD -> master) Добавил ввод имени участника
Следует обратить внимание на то, что данные способы позволяют внести изменение в описание только последнего коммита.
4.2. Коммит был запушен
4.2.1. Принудительная перезапись истории
Что описание к коммиту нужно изменить, вы вспомнили, когда он был уже отправлен на GitHub.
Чтобы решить эту задачу, необходимо к команде из предыдущего пункта добавить git push -f, которая принудительно перезапишет коммит с ошибочным описанием — исправленным:
> git commit --amend -m "Добавил ввод имени участника"
> git push -f
Среди прочего, в консоли отобразится примерно следующая строка:
+ 5aa3d6d...ce3cf6f master -> master (forced update).
Она сообщает, что было сделано принудительное обновление репозитория.
У ваших коллег (или наставника) после ввода git pull отобразятся строки, тоже сообщающие о принудительном изменении и последующем успешном слиянии (merge):
+ 5aa3d6d...ce3cf6f master     -> origin/master  (forced update)
Merge made by the 'ort' strategy.
5. Как выполнить пуш, если Git не дает это сделать?
Представим, что на первых порах вы еще не до конца освоили Git и какое-то время вносили изменения в файлы прямо на GitHub. Но рано или поздно поняли, что для своего профессионального роста все же нужно изучить базовые команды Git, и применять их в консоли.
После этого у себя на компьютере начали писать код очередного домашнего задания, а затем решили его запушить на GitHub для проверки наставником. Но сделать пуш у вас в итоге не получилось, а в консоли отобразилась ошибка:
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
Возможна и другая ситуация, когда вы пушите в один репозиторий на GitHub с разных компьютеров. Например, днем, в свободное от работы время, порешали домашку, а затем запушили ее на удаленный репозиторий. Вечером, придя домой, продолжили писать код для следующего ДЗ. А про код, запушенный на GitHub, либо забыли, либо подумали, что из-за него проблем не будет. Но в действительности оказалось все иначе.
Или на GitHub случайно попали class-файлы и вы без задней мысли пошли на сайт и удалили их.
Во всех этих ситуациях пуш приведет к одной и той же ошибке, связанной с тем, что истории изменений на локальном и удаленном репозитории пошли разными путями: на GitHub есть коммит, которого нет в ЛР. В дереве коммитов на ЛР на какое-то количество коммитов меньше, чем на УР.
Научимся решать эту проблему.
5.1. Подтягивание изменений с GitHub
Зайдя в свой репозиторий на GitHub, я внес в README.md следующие мелкие изменения:
  • добавил картинку в виде шапки
  • пункт «Командная строка» заменил на конкретное название используемой программы — cmder
  • добавил иконки к каждому пункту списка
В качестве описания к коммиту указал следующий текст:
При этом README.md содержит следующий код:
# [StartJava](https://topjava.ru/startjava) -- курс на Java для начинающих

![image](https://user-images.githubusercontent.com/29703461/194078652-25a6e509-cdc6-4af4-9ab0-78b6b336c749.png)

## Используемые на курсе инструменты и технологии

:coffee: Java

:octocat: Git/GitHub

:pager: cmder

:bookmark_tabs: Sublime Text

:fire: Intellij IDEA

:gem: SQL

:elephant: PostgreSQL

:newspaper: psql
Проделанные только что изменения я благополучно забыл подтянуть на свой компьютер с помощью git pull. И продолжил работать над классом MyFirstApp.java в ЛР, добавив в него еще один квиз и немного мелких правок.
В итоге класс (у меня на компьютере, не в GitHub) стал выглядеть так:
import java.util.Scanner;

public class MyFirstApp {
    public static void main(String[] args) {
        Scanner console = new Scanner(System.in, "cp866");
        System.out.print("Введите, пожалуйста, свое имя: ");
        String name = console.nextLine();

        System.out.println("\n1. У какого языка программирования следующий слоган:");
        System.out.print("\"Написано однажды, ");
        System.out.println("работает везде!\"");

        String answer = console.nextLine();
        if (answer.equalsIgnoreCase("Java")) {
            System.out.println(name + ", вы угадали!");
        } else System.out.println("Увы, но - это Java");

        System.out.println("\n2. Какая фамилия у автора языка Java?");

        answer = console.nextLine();
        if (answer.equals("Гослинг") || answer.equals("Gosling")) {
            System.out.println(name + ", вы угадали!");
        } else System.out.println("Увы, но - это Гослинг (Gosling)");
    }
}
Добавил изменения в коммит:
> git add MyFirstApp.java
> git commit -m "Добавил quiz по автору Java"
Отобразим полученный результат в консоли:
> git log --oneline -1
7106587 (HEAD -> master) Добавил quiz по автору Java
А теперь самое интересное: попробуем сделать push:
> git push
To https://github.com/ichimax/startjava2.git
 ! [rejected]        master -> master (fetch first)
error: failed to push some refs to 'https://github.com/ichimax/startjava2.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Отобразилась ошибка, означающая, что в локальном и удаленном репозитории истории изменений пошли разными путями и не соответствуют друг другу.
Для решения этой проблемы Git предлагает выполнить команду pull, которая подтянет коммиты с УР, а затем объединит их с локальными, чтобы выровнять историю:
> git pull
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 1.16 KiB | 62.00 KiB/s, done.
From https://github.com/ichimax/startjava2
   ce3cf6f..2bdecf7  master     -> origin/master
Merge made by the 'ort' strategy.
 README.md | 25 +++++++++++++++++--------
 1 file changed, 17 insertions(+), 8 deletions(-)
В этом сообщении стоить обратить внимание на строку Merge made by the 'ort' strategy. Из нее мы можем сделать вывод, что произошло слияние (merge).
Слияние — это объединение истории коммитов из разных веток в одну. У нас ветка хоть и одна (master), но история коммитов на GitHub и в ЛР стала с какого-то момента различаться. Из-за этого и не удавалось сделать push. Когда мы выполнили git pull, то Git автоматически объединил (что бывает не всегда) последний коммит из УР с последним коммитом из ЛР, создав новый.
Не всегда Git может выполнить слияние автоматически, что вызывает конфликт слияния. Такое бывает, когда в разных ветках был изменен один и тот же фрагмент кода. Из-за этого Git не сможет определить, какую версию использовать. Для решения проблемы потребуется вмешательство пользователя. Более подробно об этом можно узнать по ссылке.
Для того, чтобы минимизировать возникновение подобных ситуаций (конфликтов), рекомендуется каждый раз перед началом работы с проектом делать git pull, чтобы синхронизировать историю коммитов ЛР с историей УР.
Отобразим 4 последних коммита:
> git log --oneline -4
1b4a7b7 (HEAD -> master) Merge branch 'master' of https://github.com/ichimax/startjava2
7106587 Добавил quiz по автору Java
2bdecf7 (origin/master) Обновил README.md
ce3cf6f Добавил ввод имени участника
Из этого списка видно, что:
2bdecf7 — был сделан на GitHub
7106587 — был сделан в ЛР
1b4a7b7 — получился автоматически после слияния двух предыдущих коммитов
Обратите внимание, что у коммита, сделанного на GitHub, ветка называется не master, а origin/master. Разница в том, что master — это имя локальной ветки, а origin/master является удаленной веткой с именем master.
Отобразим ветки, связанные с нашим репозиторием:
> git branch -a
* master
  remotes/origin/master
Ветка master является текущей (помечена *) веткой ЛР. remotes/origin/master — это ветвь с именем master и псевдонимом origin на УР.
То, что это — разные ветки, можно убедиться визуально, введя следующую команду:
D:\Java\StartJava\src (master -> origin)
> git log --pretty=format:"%h - %s" --graph
*   1b4a7b7 - Merge branch 'master' of https://github.com/ichimax/startjava2
|\ 
| * 2bdecf7 - Обновил README.md
* | 7106587 - Добавил quiz по автору Java
|/
* ce3cf6f - Добавил ввод имени участника
* 4bf0ddd - Добавил .gitignore с маской *.class
* 90ca67c - Добавил quiz по слогану Java
* b59d871 - Изменил вывод текста, отображаемого в консоль
* 39ba195 - Переименовал about.txt в README.md и внес в него описание проекта
* 1e36e0f - Инициализация проекта
Опция --graph позволяет вывести граф в формате ASCII, который показывает текущую ветку и историю слияний. Более подробно с разными опциями команды log можно ознакомиться по ссылке.
Давайте взглянем на различия в последних коммитах в удаленной и локальной master-ветках:
D:\Java\StartJava\src (master -> origin)
> git diff origin/master..master
diff --git a/src/MyFirstApp.java b/src/MyFirstApp.java
index 8e25560..08099ad 100644
--- a/src/MyFirstApp.java
+++ b/src/MyFirstApp.java
@@ -4,15 +4,22 @@ public class MyFirstApp {
     public static void main(String[] args) {
         Scanner console = new Scanner(System.in, "cp866");
         System.out.print("Введите, пожалуйста, свое имя: ");
-        String name = console.next();
+        String name = console.nextLine();

-        System.out.println("У какого языка программирования следующий слоган:");
+       System.out.println("\n1. У какого языка программирования следующий слоган:");
         System.out.print("\"Написано однажды, ");
         System.out.println("работает везде!\"");

-        String answer = console.next();
+        String answer = console.nextLine();
         if (answer.equalsIgnoreCase("Java")) {
             System.out.println(name + ", вы угадали!");
         } else System.out.println("Увы, но - это Java");
+
+        System.out.println("\n2. Какая фамилия у автора языка Java?");
+
+        answer = console.nextLine();
+        if (answer.equals("Гослинг") || answer.equals("Gosling")) {
+            System.out.println(name + ", вы угадали!");
+        } else System.out.println("Увы, но - это Гослинг (Gosling)");
     }
 }
После всех манипуляций отобразим состояние репозитория:
> git status
On branch master
Your branch is ahead of 'origin/master' by 2 commits.
  (use "git push" to publish your local commits)
 
nothing to commit, working tree clean
Посмотрим, какие два коммита мы должны запушить:
> git log --branches --not --remotes --oneline
1b4a7b7 (HEAD -> master) Merge branch 'master' of https://github.com/ichimax/startjava2
7106587 Добавил quiz по автору Java
Выполним push и снова введем команду, отображающую список из 4 коммитов:
> git push
> git log --oneline -4
1b4a7b7 (HEAD -> master, origin/master) Merge branch 'master' of https://github.com/ichimax/startjava2
7106587 Добавил quiz по автору Java
2bdecf7 Обновил README.md
ce3cf6f Добавил ввод имени участника
Видим, что история коммитов выровнялась: HEAD в ЛР и УР указывают на самый последний коммит 1b4a7b7.
6. Список решений
Список решений тех или иных задач с помощью Git, разобранных в статье, с кратким описанием.
Заключение
В статье мы рассмотрели самые простые, но часто возникающие проблемы, которые могут появиться при работе с Git и GitHub. Это базовый минимум, который поможет вам на первых порах не чувствовать себя растерянным, столкнувшись лицом к лицу с описанными сложностями.
Автор: Чимаев Максим
Оцените статью, если она вам понравилась!