Versionsverwaltung mit Git

GitLab

Für die Software-Verwaltung der Arbeitsgruppe wird das GitLab-System genutzt, das unter https://git.ps.informatik.uni-kiel.de/ erreichbar ist. Informationen zur Struktur und Verwendung von GitLab findet man in der GitLab-Beschreibung der Arbeitsgruppe.

Hier werden im folgenden nur einige Hinweise zur Benutzung von Git gegeben.

Benutzung von Git

Very nice and short tutorial:

http://www.kernel.org/pub/software/scm/git/docs/gittutorial.html

It is a good idea to introduce yourself to git with your name and public email address before doing any operation. The easiest way to do so is:

  git config --global user.name "Michael Hanus"
  git config --global user.email mh@informatik.uni-kiel.de

Now, to get started: create a new project in Gitorious (ask the admin!) and clone it, e.g.,

  git clone git@git.ps.informatik.uni-kiel.de/theses/2013-joe.git MyThesis

This creates a new directory MyThesis which can be managed by git. Go into this directory

  cd MyThesis

The fastest way to work is git is to call

  git gui

in the created directory, as that gui is simple and well designed. Normally you do not need command line information until after some getting used to the system.

Git Prompt

Der Git Prompt erleichtert es, beim Arbeiten mit Git zu wissen, in welcher Arbeitskopie eines Git Repositories man sich gerade befindet.

Folgende Datei muss unter ~/.git-prompt erstellt werden:

# bash/zsh git prompt support
#
# Copyright (C) 2006,2007 Shawn O. Pearce <spearce@spearce.org>
# Distributed under the GNU General Public License, version 2.0.
#
# This script allows you to see repository status in your prompt.
#
# To enable:
#
#    1) Copy this file to somewhere (e.g. ~/.git-prompt.sh).
#    2) Add the following line to your .bashrc/.zshrc:
#        source ~/.git-prompt.sh
#    3a) Change your PS1 to call __git_ps1 as
#        command-substitution:
#        Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ '
#        ZSH:  setopt PROMPT_SUBST ; PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ '
#        the optional argument will be used as format string.
#    3b) Alternatively, for a slightly faster prompt, __git_ps1 can
#        be used for PROMPT_COMMAND in Bash or for precmd() in Zsh
#        with two parameters, <pre> and <post>, which are strings
#        you would put in $PS1 before and after the status string
#        generated by the git-prompt machinery.  e.g.
#        Bash: PROMPT_COMMAND='__git_ps1 "\u@\h:\w" "\\\$ "'
#          will show username, at-sign, host, colon, cwd, then
#          various status string, followed by dollar and SP, as
#          your prompt.
#        ZSH:  precmd () { __git_ps1 "%n" ":%~$ " "|%s" }
#          will show username, pipe, then various status string,
#          followed by colon, cwd, dollar and SP, as your prompt.
#        Optionally, you can supply a third argument with a printf
#        format string to finetune the output of the branch status
#
# The repository status will be displayed only if you are currently in a
# git repository. The %s token is the placeholder for the shown status.
#
# The prompt status always includes the current branch name.
#
# In addition, if you set GIT_PS1_SHOWDIRTYSTATE to a nonempty value,
# unstaged (*) and staged (+) changes will be shown next to the branch
# name.  You can configure this per-repository with the
# bash.showDirtyState variable, which defaults to true once
# GIT_PS1_SHOWDIRTYSTATE is enabled.
#
# You can also see if currently something is stashed, by setting
# GIT_PS1_SHOWSTASHSTATE to a nonempty value. If something is stashed,
# then a '$' will be shown next to the branch name.
#
# If you would like to see if there're untracked files, then you can set
# GIT_PS1_SHOWUNTRACKEDFILES to a nonempty value. If there're untracked
# files, then a '%' will be shown next to the branch name.  You can
# configure this per-repository with the bash.showUntrackedFiles
# variable, which defaults to true once GIT_PS1_SHOWUNTRACKEDFILES is
# enabled.
#
# If you would like to see the difference between HEAD and its upstream,
# set GIT_PS1_SHOWUPSTREAM="auto".  A "<" indicates you are behind, ">"
# indicates you are ahead, "<>" indicates you have diverged and "="
# indicates that there is no difference. You can further control
# behaviour by setting GIT_PS1_SHOWUPSTREAM to a space-separated list
# of values:
#
#     verbose       show number of commits ahead/behind (+/-) upstream
#     name          if verbose, then also show the upstream abbrev name
#     legacy        don't use the '--count' option available in recent
#                   versions of git-rev-list
#     git           always compare HEAD to @{upstream}
#     svn           always compare HEAD to your SVN upstream
#
# By default, __git_ps1 will compare HEAD to your SVN upstream if it can
# find one, or @{upstream} otherwise.  Once you have set
# GIT_PS1_SHOWUPSTREAM, you can override it on a per-repository basis by
# setting the bash.showUpstream config variable.
#
# If you would like to see more information about the identity of
# commits checked out as a detached HEAD, set GIT_PS1_DESCRIBE_STYLE
# to one of these values:
#
#     contains      relative to newer annotated tag (v1.6.3.2~35)
#     branch        relative to newer tag or branch (master~4)
#     describe      relative to older annotated tag (v1.6.3.1-13-gdd42c2f)
#     default       exactly matching tag
#
# If you would like a colored hint about the current dirty state, set
# GIT_PS1_SHOWCOLORHINTS to a nonempty value. The colors are based on
# the colored output of "git status -sb" and are available only when
# using __git_ps1 for PROMPT_COMMAND or precmd.

# check whether printf supports -v
__git_printf_supports_v=
printf -v __git_printf_supports_v -- '%s' yes >/dev/null 2>&1

# stores the divergence from upstream in $p
# used by GIT_PS1_SHOWUPSTREAM
__git_ps1_show_upstream ()
{
	local key value
	local svn_remote svn_url_pattern count n
	local upstream=git legacy="" verbose="" name=""

	svn_remote=()
	# get some config options from git-config
	local output="$(git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showupstream)$' 2>/dev/null | tr '\0\n' '\n ')"
	while read -r key value; do
		case "$key" in
		bash.showupstream)
			GIT_PS1_SHOWUPSTREAM="$value"
			if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]]; then
				p=""
				return
			fi
			;;
		svn-remote.*.url)
			svn_remote[$((${#svn_remote[@]} + 1))]="$value"
			svn_url_pattern="$svn_url_pattern\\|$value"
			upstream=svn+git # default upstream is SVN if available, else git
			;;
		esac
	done <<< "$output"

	# parse configuration values
	for option in ${GIT_PS1_SHOWUPSTREAM}; do
		case "$option" in
		git|svn) upstream="$option" ;;
		verbose) verbose=1 ;;
		legacy)  legacy=1  ;;
		name)    name=1 ;;
		esac
	done

	# Find our upstream
	case "$upstream" in
	git)    upstream="@{upstream}" ;;
	svn*)
		# get the upstream from the "git-svn-id: ..." in a commit message
		# (git-svn uses essentially the same procedure internally)
		local -a svn_upstream
		svn_upstream=($(git log --first-parent -1 \
					--grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null))
		if [[ 0 -ne ${#svn_upstream[@]} ]]; then
			svn_upstream=${svn_upstream[${#svn_upstream[@]} - 2]}
			svn_upstream=${svn_upstream%@*}
			local n_stop="${#svn_remote[@]}"
			for ((n=1; n <= n_stop; n++)); do
				svn_upstream=${svn_upstream#${svn_remote[$n]}}
			done

			if [[ -z "$svn_upstream" ]]; then
				# default branch name for checkouts with no layout:
				upstream=${GIT_SVN_ID:-git-svn}
			else
				upstream=${svn_upstream#/}
			fi
		elif [[ "svn+git" = "$upstream" ]]; then
			upstream="@{upstream}"
		fi
		;;
	esac

	# Find how many commits we are ahead/behind our upstream
	if [[ -z "$legacy" ]]; then
		count="$(git rev-list --count --left-right \
				"$upstream"...HEAD 2>/dev/null)"
	else
		# produce equivalent output to --count for older versions of git
		local commits
		if commits="$(git rev-list --left-right "$upstream"...HEAD 2>/dev/null)"
		then
			local commit behind=0 ahead=0
			for commit in $commits
			do
				case "$commit" in
				"<"*) ((behind++)) ;;
				*)    ((ahead++))  ;;
				esac
			done
			count="$behind	$ahead"
		else
			count=""
		fi
	fi

	# calculate the result
	if [[ -z "$verbose" ]]; then
		case "$count" in
		"") # no upstream
			p="" ;;
		"0	0") # equal to upstream
			p="=" ;;
		"0	"*) # ahead of upstream
			p=">" ;;
		*"	0") # behind upstream
			p="<" ;;
		*)	    # diverged from upstream
			p="<>" ;;
		esac
	else
		case "$count" in
		"") # no upstream
			p="" ;;
		"0	0") # equal to upstream
			p=" u=" ;;
		"0	"*) # ahead of upstream
			p=" u+${count#0	}" ;;
		*"	0") # behind upstream
			p=" u-${count%	0}" ;;
		*)	    # diverged from upstream
			p=" u+${count#*	}-${count%	*}" ;;
		esac
		if [[ -n "$count" && -n "$name" ]]; then
			__git_ps1_upstream_name=$(git rev-parse \
				--abbrev-ref "$upstream" 2>/dev/null)
			if [ $pcmode = yes ]; then
				# see the comments around the
				# __git_ps1_branch_name variable below
				p="$p \${__git_ps1_upstream_name}"
			else
				p="$p ${__git_ps1_upstream_name}"
				# not needed anymore; keep user's
				# environment clean
				unset __git_ps1_upstream_name
			fi
		fi
	fi

}

# Helper function that is meant to be called from __git_ps1.  It
# injects color codes into the appropriate gitstring variables used
# to build a gitstring.
__git_ps1_colorize_gitstring ()
{
	if [[ -n ${ZSH_VERSION-} ]]; then
		local c_red='%F{red}'
		local c_green='%F{green}'
		local c_lblue='%F{blue}'
		local c_clear='%f'
	else
		# Using \[ and \] around colors is necessary to prevent
		# issues with command line editing/browsing/completion!
		local c_red='\[\e[31m\]'
		local c_green='\[\e[32m\]'
		local c_lblue='\[\e[1;34m\]'
		local c_clear='\[\e[0m\]'
	fi
	local bad_color=$c_red
	local ok_color=$c_green
	local flags_color="$c_lblue"

	local branch_color=""
	if [ $detached = no ]; then
		branch_color="$ok_color"
	else
		branch_color="$bad_color"
	fi
	c="$branch_color$c"

	z="$c_clear$z"
	if [ "$w" = "*" ]; then
		w="$bad_color$w"
	fi
	if [ -n "$i" ]; then
		i="$ok_color$i"
	fi
	if [ -n "$s" ]; then
		s="$flags_color$s"
	fi
	if [ -n "$u" ]; then
		u="$bad_color$u"
	fi
	r="$c_clear$r"
}

# __git_ps1 accepts 0 or 1 arguments (i.e., format string)
# when called from PS1 using command substitution
# in this mode it prints text to add to bash PS1 prompt (includes branch name)
#
# __git_ps1 requires 2 or 3 arguments when called from PROMPT_COMMAND (pc)
# in that case it _sets_ PS1. The arguments are parts of a PS1 string.
# when two arguments are given, the first is prepended and the second appended
# to the state string when assigned to PS1.
# The optional third parameter will be used as printf format string to further
# customize the output of the git-status string.
# In this mode you can request colored hints using GIT_PS1_SHOWCOLORHINTS=true
__git_ps1 ()
{
	local pcmode=no
	local detached=no
	local ps1pc_start='\u@\h:\w '
	local ps1pc_end='\$ '
	local printf_format=' (%s)'

	case "$#" in
		2|3)	pcmode=yes
			ps1pc_start="$1"
			ps1pc_end="$2"
			printf_format="${3:-$printf_format}"
		;;
		0|1)	printf_format="${1:-$printf_format}"
		;;
		*)	return
		;;
	esac

	local repo_info rev_parse_exit_code
	repo_info="$(git rev-parse --git-dir --is-inside-git-dir \
		--is-bare-repository --is-inside-work-tree \
		--short HEAD 2>/dev/null)"
	rev_parse_exit_code="$?"

	if [ -z "$repo_info" ]; then
		if [ $pcmode = yes ]; then
			#In PC mode PS1 always needs to be set
			PS1="$ps1pc_start$ps1pc_end"
		fi
		return
	fi

	local short_sha
	if [ "$rev_parse_exit_code" = "0" ]; then
		short_sha="${repo_info##*$'\n'}"
		repo_info="${repo_info%$'\n'*}"
	fi
	local inside_worktree="${repo_info##*$'\n'}"
	repo_info="${repo_info%$'\n'*}"
	local bare_repo="${repo_info##*$'\n'}"
	repo_info="${repo_info%$'\n'*}"
	local inside_gitdir="${repo_info##*$'\n'}"
	local g="${repo_info%$'\n'*}"

	local r=""
	local b=""
	local step=""
	local total=""
	if [ -d "$g/rebase-merge" ]; then
		read b 2>/dev/null <"$g/rebase-merge/head-name"
		read step 2>/dev/null <"$g/rebase-merge/msgnum"
		read total 2>/dev/null <"$g/rebase-merge/end"
		if [ -f "$g/rebase-merge/interactive" ]; then
			r="|REBASE-i"
		else
			r="|REBASE-m"
		fi
	else
		if [ -d "$g/rebase-apply" ]; then
			read step 2>/dev/null <"$g/rebase-apply/next"
			read total 2>/dev/null <"$g/rebase-apply/last"
			if [ -f "$g/rebase-apply/rebasing" ]; then
				read b 2>/dev/null <"$g/rebase-apply/head-name"
				r="|REBASE"
			elif [ -f "$g/rebase-apply/applying" ]; then
				r="|AM"
			else
				r="|AM/REBASE"
			fi
		elif [ -f "$g/MERGE_HEAD" ]; then
			r="|MERGING"
		elif [ -f "$g/CHERRY_PICK_HEAD" ]; then
			r="|CHERRY-PICKING"
		elif [ -f "$g/REVERT_HEAD" ]; then
			r="|REVERTING"
		elif [ -f "$g/BISECT_LOG" ]; then
			r="|BISECTING"
		fi

		if [ -n "$b" ]; then
			:
		elif [ -h "$g/HEAD" ]; then
			# symlink symbolic ref
			b="$(git symbolic-ref HEAD 2>/dev/null)"
		else
			local head=""
			if ! read head 2>/dev/null <"$g/HEAD"; then
				if [ $pcmode = yes ]; then
					PS1="$ps1pc_start$ps1pc_end"
				fi
				return
			fi
			# is it a symbolic ref?
			b="${head#ref: }"
			if [ "$head" = "$b" ]; then
				detached=yes
				b="$(
				case "${GIT_PS1_DESCRIBE_STYLE-}" in
				(contains)
					git describe --contains HEAD ;;
				(branch)
					git describe --contains --all HEAD ;;
				(describe)
					git describe HEAD ;;
				(* | default)
					git describe --tags --exact-match HEAD ;;
				esac 2>/dev/null)" ||

				b="$short_sha..."
				b="($b)"
			fi
		fi
	fi

	if [ -n "$step" ] && [ -n "$total" ]; then
		r="$r $step/$total"
	fi

	local w=""
	local i=""
	local s=""
	local u=""
	local c=""
	local p=""

	if [ "true" = "$inside_gitdir" ]; then
		if [ "true" = "$bare_repo" ]; then
			c="BARE:"
		else
			b="GIT_DIR!"
		fi
	elif [ "true" = "$inside_worktree" ]; then
		if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ] &&
		   [ "$(git config --bool bash.showDirtyState)" != "false" ]
		then
			git diff --no-ext-diff --quiet --exit-code || w="*"
			if [ -n "$short_sha" ]; then
				git diff-index --cached --quiet HEAD -- || i="+"
			else
				i="#"
			fi
		fi
		if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ] &&
		   [ -r "$g/refs/stash" ]; then
			s="$"
		fi

		if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ] &&
		   [ "$(git config --bool bash.showUntrackedFiles)" != "false" ] &&
		   git ls-files --others --exclude-standard --error-unmatch -- '*' >/dev/null 2>/dev/null
		then
			u="%${ZSH_VERSION+%}"
		fi

		if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
			__git_ps1_show_upstream
		fi
	fi

	local z="${GIT_PS1_STATESEPARATOR-" "}"

	# NO color option unless in PROMPT_COMMAND mode
	if [ $pcmode = yes ] && [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then
		__git_ps1_colorize_gitstring
	fi

	b=${b##refs/heads/}
	if [ $pcmode = yes ]; then
		# In pcmode (and only pcmode) the contents of
		# $gitstring are subject to expansion by the shell.
		# Avoid putting the raw ref name in the prompt to
		# protect the user from arbitrary code execution via
		# specially crafted ref names (e.g., a ref named
		# '$(IFS=_;cmd=sudo_rm_-rf_/;$cmd)' would execute
		# 'sudo rm -rf /' when the prompt is drawn).  Instead,
		# put the ref name in a new global variable (in the
		# __git_ps1_* namespace to avoid colliding with the
		# user's environment) and reference that variable from
		# PS1.
		__git_ps1_branch_name=$b
		# note that the $ is escaped -- the variable will be
		# expanded later (when it's time to draw the prompt)
		b="\${__git_ps1_branch_name}"
	fi

	local f="$w$i$s$u"
	local gitstring="$c$b${f:+$z$f}$r$p"

	if [ $pcmode = yes ]; then
		if [ "${__git_printf_supports_v-}" != yes ]; then
			gitstring=$(printf -- "$printf_format" "$gitstring")
		else
			printf -v gitstring -- "$printf_format" "$gitstring"
		fi
		PS1="$ps1pc_start$gitstring$ps1pc_end"
	else
		printf -- "$printf_format" "$gitstring"
	fi
}

Und diese Datei muss beim Starten der Bash geladen werden (via .bashrc):

if [ -r  ~/.git-prompt ]; then 
        . ~/.git-prompt
        PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ '
fi

Tricks und Kniffe

Zeilenumbrüche mit Unix und Windows

Während Windows Zeilenumbrüche durch ein CRLF darstellt (carriage return/line feed), nutzt Unix nur LF. Werden unterschiedliche Systeme für dasselbe Repository verwendet ist es also sinnvoll, eine automatische Konversion vorzunehmen. Vorgeschlagen wird Folgendes:

  • intern wird LF verwendet
  • Bei Unix-Systemen werden fälschlich eingecheckte CRLF beim Auschecken in LF umgewandelt
  • Bei Windows-Systemen werden LF beim Auschecken in CRLF umgewandelt, beim Einchecken geschieht dies anders herum, CRLF werden in LF umgewandelt

Hierzu sollte man folgende Einstellung vornehmen:

Für Unix:

git config --global core.autocrlf input

Für Windows:

git config --global core.autocrlf true

Datei(en) aus Git Historie entfernen

Manchmal passiert es, dass man aus Versehen eine oder mehrere Dateien zum Repository hinzufügt. Auch wenn man diese anschließend wieder löscht bleiben sie dennoch in der Git Historie enthalten. Hat man beispielsweise ein bin-Verzeichnis eingecheckt, kann dies sehr schnell diverse MiB bedeuten die jeder Nutzer mit auschecken muss.

Um diese aus der Historie komplett zu entfernen, kann man Folgendes tun, um das bin/-Verzeichnis zu entfernen:

git filter-branch --index-filter 'git rm --cached --ignore-unmatch bin/'

Dieser Befehl entfernt den Unterordner aus der gesamten Historie. Soll die Historie erst ab einem bestimmten Commit verändert werden, geht dies mit:

git filter-branch --index-filter 'git rm --cached --ignore-unmatch bin/' -- abcd1234..^

wobei abcd1234 der entsprechende Commit ist.

Anschließend müssen noch entsprechende Verweise entfernt werden:

rm -Rf .git/refs/original
rm -Rf .git/logs/
git gc
git prune --expire now

Danach sind die Daten dauerhaft aus der Historie entfernt. Weitere Details finden sich in folgendem Artikel: Git Internals - Maintenance and Data Recovery

Farbiger Prompt

Auch wenn man gerne mit der Konsole arbeitet möchte man vielleicht etwas Farbe in seine Ausgabe bringen. Dies erreicht man einfach per:

git config --global color.ui true

Veraltete Remote Branches löschen

Durch folgenden Ablauf kann es passieren, dass man in seinem lokalen Repository eine Referenz zu einem gelöschten Remote Branch erhält:

$ git branch feature               # neuen Feature Branch erstellen
...                                # Commits
$ git push origin feature:feature  # Branch zu Remote Repository hinzufügen
...                                # weitere Commits
$ git checkout master              #
$ git merge feature                # Feature Branch in master mergen
$ git push origin :feature         # Feature Branch im Remote Repository löschen
$ git branch -D feature            # Feature Branch lokal löschen

Nun ist der Remote Branch feature aber lokal noch sichtbar:

$ git branch -a
master
remotes/origin/feature
remotes/origin/master

Um solche Branches zu löschen hilft folgendes Kommando:

$ git remote prune origin --dry-run
Pruning origin
URL: <some url>
 * [would prune] origin/feature

Mit der Option –dry-run wird nur angezeigt, was getan werden würde (nützlich zum Testen), ohne diese Option werden die Branches entfernt.

$ git remote prune origin
Pruning origin
URL: <some url>
 * [pruned] origin/feature

Ignorierte Dateien anzeigen

Möchte man sich die Dateien anzeigen lassen, die Git ignoriert (hilfreich z.B. zum Aufräumen), so hilft folgender Befehl:

git ls-files --others -i --exclude-standard

Alternativ geht auch:

git clean -ndX

Unterordner in separates Repository extrahieren

Angenommen man hat ein Repository, aus dem man einen Unterordner in ein separates Repository extrahieren möchte:

project/
  - presentation/
  - src/
  - theses/

Wir wollen nun den Ordner src/ in ein neues Repository auslagern:

git clone <url> project-src
cd project-src
git filter-branch --subdirectory-filter src HEAD

Nach dem Auschecken filtern wir also alle Commits heraus, die nicht den Ordner src betreffen, und die Inhalte werden aus src in den Hauptordner verschoben.

Nun kann man noch ein bisschen aufräumen:

git reset --hard
git gc --aggressive
git prune

Remote-Tags löschen

You probably won't need to do this often (if ever at all) but just in case, here is how to delete a tag from a remote Git repository.

If you have a tag named 12345 then you would just do this:

git tag -d 12345
git push origin :refs/tags/12345

That will remove 12345 from the remote repository.

/srv/dokuwiki/adminwiki/data/pages/user/git.txt · Zuletzt geändert: 2019-07-08 12:39 von hbs
CC Attribution-Noncommercial-Share Alike 4.0 International
Driven by DokuWiki Recent changes RSS feed Valid CSS Valid XHTML 1.0