diff options
Diffstat (limited to 'git-gui')
83 files changed, 59282 insertions, 0 deletions
diff --git a/git-gui/.gitattributes b/git-gui/.gitattributes new file mode 100644 index 0000000..59cd41d --- /dev/null +++ b/git-gui/.gitattributes @@ -0,0 +1,5 @@ +* whitespace=indent-with-non-tab,trailing-space,space-before-tab,tabwidth=4 +* encoding=US-ASCII +git-gui.sh encoding=UTF-8 +/po/*.po encoding=UTF-8 +/GIT-VERSION-GEN eol=lf diff --git a/git-gui/.gitignore b/git-gui/.gitignore new file mode 100644 index 0000000..6483b21 --- /dev/null +++ b/git-gui/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +config.mak +Git Gui.app* +git-gui.tcl +GIT-VERSION-FILE +GIT-GUI-VARS +git-gui +lib/tclIndex diff --git a/git-gui/GIT-VERSION-GEN b/git-gui/GIT-VERSION-GEN new file mode 100755 index 0000000..92373d2 --- /dev/null +++ b/git-gui/GIT-VERSION-GEN @@ -0,0 +1,80 @@ +#!/bin/sh + +GVF=GIT-VERSION-FILE +DEF_VER=0.21.GITGUI + +LF=' +' + +tree_search () +{ + head=$1 + tree=$2 + for p in $(git rev-list --parents --max-count=1 $head 2>/dev/null) + do + test $tree = $(git rev-parse $p^{tree} 2>/dev/null) && + vn=$(git describe --abbrev=4 $p 2>/dev/null) && + case "$vn" in + gitgui-[0-9]*) echo $vn; break;; + esac + done +} + +# Always use the tarball version file if found, just +# in case we are somehow contained in a larger git +# repository that doesn't actually track our state. +# (At least one package manager is doing this.) +# +# We may be a subproject, so try looking for the merge +# commit that supplied this directory content if we are +# not at the toplevel. We probably will always be the +# second parent in the commit, but we shouldn't rely on +# that fact. +# +# If we are at the toplevel or the merge assumption fails +# try looking for a gitgui-* tag. + +if test -f version && + VN=$(cat version) +then + : happy +elif prefix="$(git rev-parse --show-prefix 2>/dev/null)" + test -n "$prefix" && + head=$(git rev-list --max-count=1 HEAD -- . 2>/dev/null) && + tree=$(git rev-parse --verify "HEAD:$prefix" 2>/dev/null) && + VN=$(tree_search $head $tree) + case "$VN" in + gitgui-[0-9]*) : happy ;; + *) (exit 1) ;; + esac +then + VN=$(echo "$VN" | sed -e 's/^gitgui-//;s/-/./g'); +elif VN=$(git describe --abbrev=4 HEAD 2>/dev/null) && + case "$VN" in + gitgui-[0-9]*) : happy ;; + *) (exit 1) ;; + esac +then + VN=$(echo "$VN" | sed -e 's/^gitgui-//;s/-/./g'); +else + VN="$DEF_VER" +fi + +dirty=$(sh -c 'git diff-index --name-only HEAD' 2>/dev/null) || dirty= +case "$dirty" in +'') + ;; +*) + VN="$VN-dirty" ;; +esac + +if test -r $GVF +then + VC=$(sed -e 's/^GITGUI_VERSION = //' <$GVF) +else + VC=unset +fi +test "$VN" = "$VC" || { + echo >&2 "GITGUI_VERSION = $VN" + echo "GITGUI_VERSION = $VN" >$GVF +} diff --git a/git-gui/Makefile b/git-gui/Makefile new file mode 100644 index 0000000..f10caed --- /dev/null +++ b/git-gui/Makefile @@ -0,0 +1,347 @@ +all:: + +# Define V=1 to have a more verbose compile. +# +# Define NO_MSGFMT if you do not have msgfmt from the GNU gettext +# package and want to use our rough pure Tcl po->msg translator. +# TCL_PATH must be valid for this to work. +# + +GIT-VERSION-FILE: FORCE + @$(SHELL_PATH) ./GIT-VERSION-GEN +-include GIT-VERSION-FILE + +uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not') +uname_O := $(shell sh -c 'uname -o 2>/dev/null || echo not') +uname_R := $(shell sh -c 'uname -r 2>/dev/null || echo not') + +SCRIPT_SH = git-gui.sh +GITGUI_MAIN := git-gui +GITGUI_BUILT_INS = git-citool +ALL_LIBFILES = $(wildcard lib/*.tcl) +PRELOAD_FILES = lib/class.tcl +NONTCL_LIBFILES = \ + lib/git-gui.ico \ + $(wildcard lib/win32_*.js) \ +#end NONTCL_LIBFILES + +ifndef SHELL_PATH + SHELL_PATH = /bin/sh +endif + +ifndef gitexecdir + gitexecdir := $(shell git --exec-path) +endif + +ifndef sharedir +ifeq (git-core,$(notdir $(gitexecdir))) + sharedir := $(dir $(patsubst %/,%,$(dir $(gitexecdir))))share +else + sharedir := $(dir $(gitexecdir))share +endif +endif + +ifndef INSTALL + INSTALL = install +endif + +RM_RF ?= rm -rf +RMDIR ?= rmdir + +INSTALL_D0 = $(INSTALL) -d -m 755 # space is required here +INSTALL_D1 = +INSTALL_R0 = $(INSTALL) -m 644 # space is required here +INSTALL_R1 = +INSTALL_X0 = $(INSTALL) -m 755 # space is required here +INSTALL_X1 = +INSTALL_A0 = find # space is required here +INSTALL_A1 = | cpio -pud +INSTALL_L0 = rm -f # space is required here +INSTALL_L1 = && ln # space is required here +INSTALL_L2 = +INSTALL_L3 = + +REMOVE_D0 = $(RMDIR) # space is required here +REMOVE_D1 = || true +REMOVE_F0 = $(RM_RF) # space is required here +REMOVE_F1 = +CLEAN_DST = true + +ifndef V + QUIET = @ + QUIET_GEN = $(QUIET)echo ' ' GEN '$@' && + QUIET_INDEX = $(QUIET)echo ' ' INDEX $(dir $@) && + QUIET_MSGFMT0 = $(QUIET)printf ' MSGFMT %12s ' $@ && v=` + QUIET_MSGFMT1 = 2>&1` && echo "$$v" | sed -e 's/fuzzy translations/fuzzy/' | sed -e 's/ messages*//g' + QUIET_2DEVNULL = 2>/dev/null + + INSTALL_D0 = dir= + INSTALL_D1 = && echo ' ' DEST $$dir && $(INSTALL) -d -m 755 "$$dir" + INSTALL_R0 = src= + INSTALL_R1 = && echo ' ' INSTALL 644 `basename $$src` && $(INSTALL) -m 644 $$src + INSTALL_X0 = src= + INSTALL_X1 = && echo ' ' INSTALL 755 `basename $$src` && $(INSTALL) -m 755 $$src + INSTALL_A0 = src= + INSTALL_A1 = && echo ' ' INSTALL ' ' `basename "$$src"` && find "$$src" | cpio -pud + + INSTALL_L0 = dst= + INSTALL_L1 = && src= + INSTALL_L2 = && dst= + INSTALL_L3 = && echo ' ' 'LINK ' `basename "$$dst"` '->' `basename "$$src"` && rm -f "$$dst" && ln "$$src" "$$dst" + + CLEAN_DST = echo ' ' UNINSTALL + REMOVE_D0 = dir= + REMOVE_D1 = && echo ' ' REMOVE $$dir && test -d "$$dir" && $(RMDIR) "$$dir" || true + REMOVE_F0 = dst= + REMOVE_F1 = && echo ' ' REMOVE `basename "$$dst"` && $(RM_RF) "$$dst" +endif + +TCLTK_PATH ?= wish +ifeq (./,$(dir $(TCLTK_PATH))) + TCL_PATH ?= $(subst wish,tclsh,$(TCLTK_PATH)) +else + TCL_PATH ?= $(dir $(TCLTK_PATH))$(notdir $(subst wish,tclsh,$(TCLTK_PATH))) +endif + +ifeq ($(uname_S),Darwin) + TKFRAMEWORK = /Library/Frameworks/Tk.framework/Resources/Wish.app + ifeq ($(shell echo "$(uname_R)" | awk -F. '{if ($$1 >= 9) print "y"}')_$(shell test -d $(TKFRAMEWORK) || echo n),y_n) + TKFRAMEWORK = /System/Library/Frameworks/Tk.framework/Resources/Wish.app + ifeq ($(shell test -d $(TKFRAMEWORK) || echo n),n) + TKFRAMEWORK = /System/Library/Frameworks/Tk.framework/Resources/Wish\ Shell.app + endif + endif + TKEXECUTABLE = $(shell basename "$(TKFRAMEWORK)" .app) +endif + +ifeq ($(findstring $(MAKEFLAGS),s),s) +QUIET_GEN = +endif + +-include config.mak + +DESTDIR_SQ = $(subst ','\'',$(DESTDIR)) +gitexecdir_SQ = $(subst ','\'',$(gitexecdir)) +SHELL_PATH_SQ = $(subst ','\'',$(SHELL_PATH)) +TCL_PATH_SQ = $(subst ','\'',$(TCL_PATH)) +TCLTK_PATH_SQ = $(subst ','\'',$(TCLTK_PATH)) +TCLTK_PATH_SED = $(subst ','\'',$(subst \,\\,$(TCLTK_PATH))) + +gg_libdir ?= $(sharedir)/git-gui/lib +libdir_SQ = $(subst ','\'',$(gg_libdir)) +libdir_SED = $(subst ','\'',$(subst \,\\,$(gg_libdir_sed_in))) +exedir = $(dir $(gitexecdir))share/git-gui/lib + +GITGUI_SCRIPT := $$0 +GITGUI_RELATIVE := +GITGUI_MACOSXAPP := + +ifeq ($(uname_O),Cygwin) + GITGUI_SCRIPT := `cygpath --windows --absolute "$(GITGUI_SCRIPT)"` + + # Is this a Cygwin Tcl/Tk binary? If so it knows how to do + # POSIX path translation just like cygpath does and we must + # keep libdir in POSIX format so Cygwin packages of git-gui + # work no matter where the user installs them. + # + ifeq ($(shell echo 'puts [file normalize /]' | '$(TCL_PATH_SQ)'),$(shell cygpath --mixed --absolute /)) + gg_libdir_sed_in := $(gg_libdir) + else + gg_libdir_sed_in := $(shell cygpath --windows --absolute "$(gg_libdir)") + endif +else + ifeq ($(exedir),$(gg_libdir)) + GITGUI_RELATIVE := 1 + endif + gg_libdir_sed_in := $(gg_libdir) +endif +ifeq ($(uname_S),Darwin) + ifeq ($(shell test -d $(TKFRAMEWORK) && echo y),y) + GITGUI_MACOSXAPP := YesPlease + endif +endif +ifneq (,$(findstring MINGW,$(uname_S))) +ifeq ($(shell expr "$(uname_R)" : '1\.'),2) + NO_MSGFMT=1 +endif + GITGUI_WINDOWS_WRAPPER := YesPlease + GITGUI_RELATIVE := 1 +endif + +ifdef GITGUI_MACOSXAPP +GITGUI_MAIN := git-gui.tcl + +git-gui: GIT-VERSION-FILE GIT-GUI-VARS + $(QUIET_GEN)rm -f $@ $@+ && \ + echo '#!$(SHELL_PATH_SQ)' >$@+ && \ + echo 'if test "z$$*" = zversion ||' >>$@+ && \ + echo ' test "z$$*" = z--version' >>$@+ && \ + echo then >>$@+ && \ + echo ' 'echo \'git-gui version '$(GITGUI_VERSION)'\' >>$@+ && \ + echo else >>$@+ && \ + echo ' libdir="$${GIT_GUI_LIB_DIR:-$(libdir_SQ)}"' >>$@+ && \ + echo ' 'exec \"'$$libdir/Git Gui.app/Contents/MacOS/$(subst \,,$(TKEXECUTABLE))'\" \ + '"$$0" "$$@"' >>$@+ && \ + echo fi >>$@+ && \ + chmod +x $@+ && \ + mv $@+ $@ + +Git\ Gui.app: GIT-VERSION-FILE GIT-GUI-VARS \ + macosx/Info.plist \ + macosx/git-gui.icns \ + macosx/AppMain.tcl \ + $(TKFRAMEWORK)/Contents/MacOS/$(TKEXECUTABLE) + $(QUIET_GEN)rm -rf '$@' '$@'+ && \ + mkdir -p '$@'+/Contents/MacOS && \ + mkdir -p '$@'+/Contents/Resources/Scripts && \ + cp '$(subst ','\'',$(subst \,,$(TKFRAMEWORK)/Contents/MacOS/$(TKEXECUTABLE)))' \ + '$@'+/Contents/MacOS && \ + cp macosx/git-gui.icns '$@'+/Contents/Resources && \ + sed -e 's/@@GITGUI_VERSION@@/$(GITGUI_VERSION)/g' \ + -e 's/@@GITGUI_TKEXECUTABLE@@/$(TKEXECUTABLE)/g' \ + macosx/Info.plist \ + >'$@'+/Contents/Info.plist && \ + sed -e 's|@@gitexecdir@@|$(gitexecdir_SQ)|' \ + -e 's|@@GITGUI_LIBDIR@@|$(libdir_SED)|' \ + macosx/AppMain.tcl \ + >'$@'+/Contents/Resources/Scripts/AppMain.tcl && \ + mv '$@'+ '$@' +endif + +ifdef GITGUI_WINDOWS_WRAPPER +GITGUI_MAIN := git-gui.tcl + +git-gui: windows/git-gui.sh + cp $< $@ +endif + +$(GITGUI_MAIN): git-gui.sh GIT-VERSION-FILE GIT-GUI-VARS + $(QUIET_GEN)rm -f $@ $@+ && \ + sed -e '1s|#!.*/sh|#!$(SHELL_PATH_SQ)|' \ + -e 's|@@SHELL_PATH@@|$(SHELL_PATH_SQ)|' \ + -e '1,30s|^ argv0=$$0| argv0=$(GITGUI_SCRIPT)|' \ + -e '1,30s|^ exec wish | exec '\''$(TCLTK_PATH_SED)'\'' |' \ + -e 's/@@GITGUI_VERSION@@/$(GITGUI_VERSION)/g' \ + -e 's|@@GITGUI_RELATIVE@@|$(GITGUI_RELATIVE)|' \ + -e '$(GITGUI_RELATIVE)s|@@GITGUI_LIBDIR@@|$(libdir_SED)|' \ + git-gui.sh >$@+ && \ + chmod +x $@+ && \ + mv $@+ $@ + +XGETTEXT ?= xgettext +ifdef NO_MSGFMT + MSGFMT ?= $(TCL_PATH) po/po2msg.sh +else + MSGFMT ?= msgfmt + ifneq ($(shell $(MSGFMT) --tcl -l C -d . /dev/null 2>/dev/null; echo $$?),0) + MSGFMT := $(TCL_PATH) po/po2msg.sh + endif +endif + +msgsdir = $(gg_libdir)/msgs +msgsdir_SQ = $(subst ','\'',$(msgsdir)) +PO_TEMPLATE = po/git-gui.pot +ALL_POFILES = $(wildcard po/*.po) +ALL_MSGFILES = $(subst .po,.msg,$(ALL_POFILES)) + +$(PO_TEMPLATE): $(SCRIPT_SH) $(ALL_LIBFILES) + $(XGETTEXT) -kmc -LTcl -o $@ $(SCRIPT_SH) $(ALL_LIBFILES) +update-po:: $(PO_TEMPLATE) + $(foreach p, $(ALL_POFILES), echo Updating $p ; msgmerge -U $p $(PO_TEMPLATE) ; ) +$(ALL_MSGFILES): %.msg : %.po + $(QUIET_MSGFMT0)$(MSGFMT) --statistics --tcl -l $(basename $(notdir $<)) -d $(dir $@) $< $(QUIET_MSGFMT1) + +lib/tclIndex: $(ALL_LIBFILES) GIT-GUI-VARS + $(QUIET_INDEX)if echo \ + $(foreach p,$(PRELOAD_FILES),source $p\;) \ + auto_mkindex lib $(patsubst lib/%,%,$(sort $(ALL_LIBFILES))) \ + | $(TCL_PATH) $(QUIET_2DEVNULL); then : ok; \ + else \ + echo >&2 " * $(TCL_PATH) failed; using unoptimized loading"; \ + rm -f $@ ; \ + echo '# Autogenerated by git-gui Makefile' >$@ && \ + echo >>$@ && \ + $(foreach p,$(PRELOAD_FILES) $(sort $(ALL_LIBFILES)),echo '$(subst lib/,,$p)' >>$@ &&) \ + echo >>$@ ; \ + fi + +TRACK_VARS = \ + $(subst ','\'',SHELL_PATH='$(SHELL_PATH_SQ)') \ + $(subst ','\'',TCL_PATH='$(TCL_PATH_SQ)') \ + $(subst ','\'',TCLTK_PATH='$(TCLTK_PATH_SQ)') \ + $(subst ','\'',gitexecdir='$(gitexecdir_SQ)') \ + $(subst ','\'',gg_libdir='$(libdir_SQ)') \ + GITGUI_MACOSXAPP=$(GITGUI_MACOSXAPP) \ +#end TRACK_VARS + +GIT-GUI-VARS: FORCE + @VARS='$(TRACK_VARS)'; \ + if test x"$$VARS" != x"`cat $@ 2>/dev/null`" ; then \ + echo >&2 " * new locations or Tcl/Tk interpreter"; \ + echo >$@ "$$VARS"; \ + fi + +ifdef GITGUI_MACOSXAPP +all:: git-gui Git\ Gui.app +endif +ifdef GITGUI_WINDOWS_WRAPPER +all:: git-gui +endif +all:: $(GITGUI_MAIN) lib/tclIndex $(ALL_MSGFILES) + +install: all + $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(gitexecdir_SQ)' $(INSTALL_D1) + $(QUIET)$(INSTALL_X0)git-gui $(INSTALL_X1) '$(DESTDIR_SQ)$(gitexecdir_SQ)' + $(QUIET)$(INSTALL_X0)git-gui--askpass $(INSTALL_X1) '$(DESTDIR_SQ)$(gitexecdir_SQ)' + $(QUIET)$(foreach p,$(GITGUI_BUILT_INS), $(INSTALL_L0)'$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' $(INSTALL_L1)'$(DESTDIR_SQ)$(gitexecdir_SQ)/git-gui' $(INSTALL_L2)'$(DESTDIR_SQ)$(gitexecdir_SQ)/$p' $(INSTALL_L3) &&) true +ifdef GITGUI_WINDOWS_WRAPPER + $(QUIET)$(INSTALL_R0)git-gui.tcl $(INSTALL_R1) '$(DESTDIR_SQ)$(gitexecdir_SQ)' +endif + $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(libdir_SQ)' $(INSTALL_D1) + $(QUIET)$(INSTALL_R0)lib/tclIndex $(INSTALL_R1) '$(DESTDIR_SQ)$(libdir_SQ)' +ifdef GITGUI_MACOSXAPP + $(QUIET)$(INSTALL_A0)'Git Gui.app' $(INSTALL_A1) '$(DESTDIR_SQ)$(libdir_SQ)' + $(QUIET)$(INSTALL_X0)git-gui.tcl $(INSTALL_X1) '$(DESTDIR_SQ)$(libdir_SQ)' +endif + $(QUIET)$(foreach p,$(ALL_LIBFILES) $(NONTCL_LIBFILES), $(INSTALL_R0)$p $(INSTALL_R1) '$(DESTDIR_SQ)$(libdir_SQ)' &&) true + $(QUIET)$(INSTALL_D0)'$(DESTDIR_SQ)$(msgsdir_SQ)' $(INSTALL_D1) + $(QUIET)$(foreach p,$(ALL_MSGFILES), $(INSTALL_R0)$p $(INSTALL_R1) '$(DESTDIR_SQ)$(msgsdir_SQ)' &&) true + +uninstall: + $(QUIET)$(CLEAN_DST) '$(DESTDIR_SQ)$(gitexecdir_SQ)' + $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(gitexecdir_SQ)'/git-gui $(REMOVE_F1) + $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(gitexecdir_SQ)'/git-gui--askpass $(REMOVE_F1) + $(QUIET)$(foreach p,$(GITGUI_BUILT_INS), $(REMOVE_F0)'$(DESTDIR_SQ)$(gitexecdir_SQ)'/$p $(REMOVE_F1) &&) true +ifdef GITGUI_WINDOWS_WRAPPER + $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(gitexecdir_SQ)'/git-gui.tcl $(REMOVE_F1) +endif + $(QUIET)$(CLEAN_DST) '$(DESTDIR_SQ)$(libdir_SQ)' + $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)'/tclIndex $(REMOVE_F1) +ifdef GITGUI_MACOSXAPP + $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)/Git Gui.app' $(REMOVE_F1) + $(QUIET)$(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)'/git-gui.tcl $(REMOVE_F1) +endif + $(QUIET)$(foreach p,$(ALL_LIBFILES) $(NONTCL_LIBFILES), $(REMOVE_F0)'$(DESTDIR_SQ)$(libdir_SQ)'/$(notdir $p) $(REMOVE_F1) &&) true + $(QUIET)$(CLEAN_DST) '$(DESTDIR_SQ)$(msgsdir_SQ)' + $(QUIET)$(foreach p,$(ALL_MSGFILES), $(REMOVE_F0)'$(DESTDIR_SQ)$(msgsdir_SQ)'/$(notdir $p) $(REMOVE_F1) &&) true + $(QUIET)$(REMOVE_D0)'$(DESTDIR_SQ)$(gitexecdir_SQ)' $(REMOVE_D1) + $(QUIET)$(REMOVE_D0)'$(DESTDIR_SQ)$(msgsdir_SQ)' $(REMOVE_D1) + $(QUIET)$(REMOVE_D0)'$(DESTDIR_SQ)$(libdir_SQ)' $(REMOVE_D1) + $(QUIET)$(REMOVE_D0)`dirname '$(DESTDIR_SQ)$(libdir_SQ)'` $(REMOVE_D1) + +dist-version: + @mkdir -p $(TARDIR) + @echo $(GITGUI_VERSION) > $(TARDIR)/version + +clean:: + $(RM_RF) $(GITGUI_MAIN) lib/tclIndex po/*.msg + $(RM_RF) GIT-VERSION-FILE GIT-GUI-VARS +ifdef GITGUI_MACOSXAPP + $(RM_RF) 'Git Gui.app'* git-gui +endif +ifdef GITGUI_WINDOWS_WRAPPER + $(RM_RF) git-gui +endif + +.PHONY: all install uninstall dist-version clean +.PHONY: FORCE diff --git a/git-gui/README.md b/git-gui/README.md new file mode 100644 index 0000000..5ce2122 --- /dev/null +++ b/git-gui/README.md @@ -0,0 +1,174 @@ +# Git GUI - A graphical user interface for Git + +Git GUI allows you to use the [Git source control management +tools](https://git-scm.com/) via a GUI. This includes staging, committing, +adding, pushing, etc. It can also be used as a blame viewer, a tree browser, +and a citool (make exactly one commit before exiting and returning to shell). +More details about Git GUI can be found in its manual page by either running +`man git-gui`, or by visiting the [online manual +page](https://git-scm.com/docs/git-gui). + +Git GUI was initially written by Shawn O. Pearce, and is distributed with the +standard Git installation. + +# Building and installing + +You need to have the following dependencies installed before you begin: + +- Git +- Tcl +- Tk +- wish +- Gitk (needed for browsing history) +- msgfmt + +Most of Git GUI is written in Tcl, so there is no compilation involved. Still, +some things do need to be done (mostly some substitutions), so you do need to +"build" it. + +You can build Git GUI using: + +``` +make +``` + +And then install it using: + +``` +make install +``` + +You probably need to have root/admin permissions to install. + +# Contributing + +The project is currently maintained by Pratyush Yadav over at +https://github.com/prati0100/git-gui. Even though the project is hosted at +GitHub, the development does not happen over GitHub Issues and Pull Requests. +Instead, an email based workflow is used. The Git mailing list +[git@vger.kernel.org](mailto:git@vger.kernel.org) is where the patches are +discussed and reviewed. + +More information about the Git mailing list and instructions to subscribe can +be found [here](https://git.wiki.kernel.org/index.php/GitCommunity). + +## Sending your changes + +Since the development happens over email, you need to send in your commits in +text format. Commits can be converted to emails via the two tools provided by +Git: `git-send-email` and `git-format-patch`. + +You can use `git-format-patch` to generate patches in mbox format from your +commits that can then be sent via email. Let's say you are working on a branch +called 'foo' that was created on top of 'master'. You can run: + +``` +git format-patch -o output_dir master..foo +``` + +to convert all the extra commits in 'foo' into a set of patches saved in the +folder `output_dir`. + +If you are sending multiple patches, it is recommended to include a cover +letter. A cover letter is an email explaining in brief what the series is +supposed to do. A cover letter template can be generated by passing +`--cover-letter` to `git-format-patch`. + +After you send your patches, you might get a review suggesting some changes. +Make those changes, and re-send your patch(es) in reply to the first patch of +your initial version. Also please mention the version of the patch. This can be +done by passing `-v X` to `git-format-patch`, where 'X' is the version number +of the patch(es). + +### Using git-send-email + +You can use `git-send-email` to send patches generated via `git-format-patch`. +While you can directly send patches via `git-send-email`, it is recommended +that you first use `git-format-patch` to generate the emails, audit them, and +then send them via `git-send-email`. + +A pretty good guide to configuring and using `git-send-email` can be found +[here](https://www.freedesktop.org/wiki/Software/PulseAudio/HowToUseGitSendEmail/) + +### Using your email client + +If your email client supports sending mbox format emails, you can use +`git-format-patch` to get an mbox file for each commit, and then send them. If +there is more than one patch in the series, then all patches after the first +patch (or the cover letter) need to be sent as replies to the first. +`git-send-email` does this by default. + +### Using GitGitGadget + +Since some people prefer a GitHub pull request based workflow, they can use +[GitGitGadget](https://gitgitgadget.github.io/) to send in patches. The tool +was originally written for sending patches to the Git project, but it now also +supports sending patches for git-gui. + +Instructions for using GitGitGadget to send git-gui patches, courtesy of +Johannes Schindelin: + +If you don't already have a fork of the [git/git](https://github.com/git/git) +repo, you need to make one. Then clone your fork: + +``` +git clone https://github.com/<your-username>/git +``` + +Then add GitGitGadget as a remote: + +``` +git remote add gitgitgadget https://github.com/gitgitgadget/git +``` + +Then fetch the git-gui branch: + +``` +git fetch gitgitgadget git-gui/master +``` + +Then create a new branch based on git-gui/master: + +``` +git checkout -b <your-branch-name> git-gui/master +``` + +Make whatever commits you need to, push them to your fork, and then head over +to https://github.com/gitgitgadget/git/pulls and open a Pull Request targeting +git-gui/master. + +GitGitGadget will welcome you with a (hopefully) helpful message. + +## Signing off + +You need to sign off your commits before sending them to the list. You can do +that by passing the `-s` option to `git-commit`. You can also use the "Sign +Off" option in Git GUI. + +A sign-off is a simple 'Signed-off-by: A U Thor \<author@example.com\>' line at +the end of the commit message, after your explanation of the commit. + +A sign-off means that you are legally allowed to send the code, and it serves +as a certificate of origin. More information can be found at +[developercertificate.org](https://developercertificate.org/). + +## Responding to review comments + +It is quite likely your patches will get review comments. Those comments are +sent on the Git mailing list as replies to your patch, and you will usually be +Cc'ed in those replies. + +You are expected to respond by either explaining your code further to convince +the reviewer what you are doing is correct, or acknowledge the comments and +re-send the patches with those comments addressed. + +Some tips for those not familiar with communication on a mailing list: + +- Use only plain text emails. No HTML at all. +- Wrap lines at around 75 characters. +- Do not send attachments. If you do need to send some files, consider using a + hosting service, and paste the link in your email. +- Do not [top post](http://www.idallen.com/topposting.html). +- Always "reply all". Keep all correspondents and the list in Cc. If you reply + directly to a reviewer, and not Cc the list, other people would not be able + to chime in. diff --git a/git-gui/git-gui--askpass b/git-gui/git-gui--askpass new file mode 100755 index 0000000..1c99ee8 --- /dev/null +++ b/git-gui/git-gui--askpass @@ -0,0 +1,71 @@ +#!/bin/sh +# Tcl ignores the next line -*- tcl -*- \ +exec wish "$0" -- "$@" + +# This is a trivial implementation of an SSH_ASKPASS handler. +# Git-gui uses this script if none are already configured. + +package require Tk + +set answer {} +set yesno 0 +set rc 255 + +if {$argc < 1} { + set prompt "Enter your OpenSSH passphrase:" +} else { + set prompt [join $argv " "] + if {[regexp -nocase {\(yes\/no\)\?\s*$} $prompt]} { + set yesno 1 + } +} + +message .m -text $prompt -justify center -aspect 4000 +pack .m -side top -fill x -padx 20 -pady 20 -expand 1 + +entry .e -textvariable answer -width 50 +pack .e -side top -fill x -padx 10 -pady 10 + +if {!$yesno} { + .e configure -show "*" +} + +frame .b +button .b.ok -text OK -command finish +button .b.cancel -text Cancel -command cancel + +pack .b.ok -side left -expand 1 +pack .b.cancel -side right -expand 1 +pack .b -side bottom -fill x -padx 10 -pady 10 + +bind . <Visibility> {focus -force .e} +bind . <Key-Return> [list .b.ok invoke] +bind . <Key-Escape> [list .b.cancel invoke] +bind . <Destroy> {set rc $rc} + +proc cancel {} { + set ::rc 255 +} + +proc finish {} { + if {$::yesno} { + if {$::answer ne "yes" && $::answer ne "no"} { + tk_messageBox -icon error -title "Error" -type ok \ + -message "Only 'yes' or 'no' input allowed." + return + } + } + + # On Windows, force the encoding to UTF-8: it is what `git.exe` expects + if {$::tcl_platform(platform) eq {windows}} { + set ::answer [encoding convertto utf-8 $::answer] + } + + puts $::answer + set ::rc 0 +} + +wm title . "OpenSSH" +tk::PlaceWindow . +vwait rc +exit $rc diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh new file mode 100755 index 0000000..4610e4c --- /dev/null +++ b/git-gui/git-gui.sh @@ -0,0 +1,4191 @@ +#!/bin/sh +# Tcl ignores the next line -*- tcl -*- \ + if test "z$*" = zversion \ + || test "z$*" = z--version; \ + then \ + echo 'git-gui version @@GITGUI_VERSION@@'; \ + exit; \ + fi; \ + argv0=$0; \ + exec wish "$argv0" -- "$@" + +set appvers {@@GITGUI_VERSION@@} +set copyright [string map [list (c) \u00a9] { +Copyright (c) 2006-2010 Shawn Pearce, et. al. + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, see <http://www.gnu.org/licenses/>.}] + +###################################################################### +## +## Tcl/Tk sanity check + +if {[catch {package require Tcl 8.5} err] + || [catch {package require Tk 8.5} err] +} { + catch {wm withdraw .} + tk_messageBox \ + -icon error \ + -type ok \ + -title "git-gui: fatal error" \ + -message $err + exit 1 +} + +catch {rename send {}} ; # What an evil concept... + +###################################################################### +## +## locate our library + +if { [info exists ::env(GIT_GUI_LIB_DIR) ] } { + set oguilib $::env(GIT_GUI_LIB_DIR) +} else { + set oguilib {@@GITGUI_LIBDIR@@} +} +set oguirel {@@GITGUI_RELATIVE@@} +if {$oguirel eq {1}} { + set oguilib [file dirname [file normalize $argv0]] + if {[file tail $oguilib] eq {git-core}} { + set oguilib [file dirname $oguilib] + } + set oguilib [file dirname $oguilib] + set oguilib [file join $oguilib share git-gui lib] + set oguimsg [file join $oguilib msgs] +} elseif {[string match @@* $oguirel]} { + set oguilib [file join [file dirname [file normalize $argv0]] lib] + set oguimsg [file join [file dirname [file normalize $argv0]] po] +} else { + set oguimsg [file join $oguilib msgs] +} +unset oguirel + +###################################################################### +## +## enable verbose loading? + +if {![catch {set _verbose $env(GITGUI_VERBOSE)}]} { + unset _verbose + rename auto_load real__auto_load + proc auto_load {name args} { + puts stderr "auto_load $name" + return [uplevel 1 real__auto_load $name $args] + } + rename source real__source + proc source {args} { + puts stderr "source $args" + uplevel 1 [linsert $args 0 real__source] + } + if {[tk windowingsystem] eq "win32"} { console show } +} + +###################################################################### +## +## Internationalization (i18n) through msgcat and gettext. See +## http://www.gnu.org/software/gettext/manual/html_node/Tcl.html + +package require msgcat + +# Check for Windows 7 MUI language pack (missed by msgcat < 1.4.4) +if {[tk windowingsystem] eq "win32" + && [package vcompare [package provide msgcat] 1.4.4] < 0 +} then { + proc _mc_update_locale {} { + set key {HKEY_CURRENT_USER\Control Panel\Desktop} + if {![catch { + package require registry + set uilocale [registry get $key "PreferredUILanguages"] + msgcat::ConvertLocale [string map {- _} [lindex $uilocale 0]] + } uilocale]} { + if {[string length $uilocale] > 0} { + msgcat::mclocale $uilocale + } + } + } + _mc_update_locale +} + +proc _mc_trim {fmt} { + set cmk [string first @@ $fmt] + if {$cmk > 0} { + return [string range $fmt 0 [expr {$cmk - 1}]] + } + return $fmt +} + +proc mc {en_fmt args} { + set fmt [_mc_trim [::msgcat::mc $en_fmt]] + if {[catch {set msg [eval [list format $fmt] $args]} err]} { + set msg [eval [list format [_mc_trim $en_fmt]] $args] + } + return $msg +} + +proc strcat {args} { + return [join $args {}] +} + +::msgcat::mcload $oguimsg +unset oguimsg + +###################################################################### +## +## On Mac, bring the current Wish process window to front + +if {[tk windowingsystem] eq "aqua"} { + catch { + exec osascript -e [format { + tell application "System Events" + set frontmost of processes whose unix id is %d to true + end tell + } [pid]] + } +} + +###################################################################### +## +## read only globals + +set _appname {Git Gui} +set _gitdir {} +set _gitworktree {} +set _isbare {} +set _gitexec {} +set _githtmldir {} +set _reponame {} +set _iscygwin {} +set _search_path {} +set _shellpath {@@SHELL_PATH@@} + +set _trace [lsearch -exact $argv --trace] +if {$_trace >= 0} { + set argv [lreplace $argv $_trace $_trace] + set _trace 1 + if {[tk windowingsystem] eq "win32"} { console show } +} else { + set _trace 0 +} + +# variable for the last merged branch (useful for a default when deleting +# branches). +set _last_merged_branch {} + +proc shellpath {} { + global _shellpath env + if {[string match @@* $_shellpath]} { + if {[info exists env(SHELL)]} { + return $env(SHELL) + } else { + return /bin/sh + } + } + return $_shellpath +} + +proc appname {} { + global _appname + return $_appname +} + +proc gitdir {args} { + global _gitdir + if {$args eq {}} { + return $_gitdir + } + return [eval [list file join $_gitdir] $args] +} + +proc gitexec {args} { + global _gitexec + if {$_gitexec eq {}} { + if {[catch {set _gitexec [git --exec-path]} err]} { + error "Git not installed?\n\n$err" + } + if {[is_Cygwin]} { + set _gitexec [exec cygpath \ + --windows \ + --absolute \ + $_gitexec] + } else { + set _gitexec [file normalize $_gitexec] + } + } + if {$args eq {}} { + return $_gitexec + } + return [eval [list file join $_gitexec] $args] +} + +proc githtmldir {args} { + global _githtmldir + if {$_githtmldir eq {}} { + if {[catch {set _githtmldir [git --html-path]}]} { + # Git not installed or option not yet supported + return {} + } + if {[is_Cygwin]} { + set _githtmldir [exec cygpath \ + --windows \ + --absolute \ + $_githtmldir] + } else { + set _githtmldir [file normalize $_githtmldir] + } + } + if {$args eq {}} { + return $_githtmldir + } + return [eval [list file join $_githtmldir] $args] +} + +proc reponame {} { + return $::_reponame +} + +proc is_MacOSX {} { + if {[tk windowingsystem] eq {aqua}} { + return 1 + } + return 0 +} + +proc is_Windows {} { + if {$::tcl_platform(platform) eq {windows}} { + return 1 + } + return 0 +} + +proc is_Cygwin {} { + global _iscygwin + if {$_iscygwin eq {}} { + if {$::tcl_platform(platform) eq {windows}} { + if {[catch {set p [exec cygpath --windir]} err]} { + set _iscygwin 0 + } else { + set _iscygwin 1 + # Handle MSys2 which is only cygwin when MSYSTEM is MSYS. + if {[info exists ::env(MSYSTEM)] && $::env(MSYSTEM) ne "MSYS"} { + set _iscygwin 0 + } + } + } else { + set _iscygwin 0 + } + } + return $_iscygwin +} + +proc is_enabled {option} { + global enabled_options + if {[catch {set on $enabled_options($option)}]} {return 0} + return $on +} + +proc enable_option {option} { + global enabled_options + set enabled_options($option) 1 +} + +proc disable_option {option} { + global enabled_options + set enabled_options($option) 0 +} + +###################################################################### +## +## config + +proc is_many_config {name} { + switch -glob -- $name { + gui.recentrepo - + remote.*.fetch - + remote.*.push + {return 1} + * + {return 0} + } +} + +proc is_config_true {name} { + global repo_config + if {[catch {set v $repo_config($name)}]} { + return 0 + } + set v [string tolower $v] + if {$v eq {} || $v eq {true} || $v eq {1} || $v eq {yes} || $v eq {on}} { + return 1 + } else { + return 0 + } +} + +proc is_config_false {name} { + global repo_config + if {[catch {set v $repo_config($name)}]} { + return 0 + } + set v [string tolower $v] + if {$v eq {false} || $v eq {0} || $v eq {no} || $v eq {off}} { + return 1 + } else { + return 0 + } +} + +proc get_config {name} { + global repo_config + if {[catch {set v $repo_config($name)}]} { + return {} + } else { + return $v + } +} + +proc is_bare {} { + global _isbare + global _gitdir + global _gitworktree + + if {$_isbare eq {}} { + if {[catch { + set _bare [git rev-parse --is-bare-repository] + switch -- $_bare { + true { set _isbare 1 } + false { set _isbare 0} + default { throw } + } + }]} { + if {[is_config_true core.bare] + || ($_gitworktree eq {} + && [lindex [file split $_gitdir] end] ne {.git})} { + set _isbare 1 + } else { + set _isbare 0 + } + } + } + return $_isbare +} + +###################################################################### +## +## handy utils + +proc _trace_exec {cmd} { + if {!$::_trace} return + set d {} + foreach v $cmd { + if {$d ne {}} { + append d { } + } + if {[regexp {[ \t\r\n'"$?*]} $v]} { + set v [sq $v] + } + append d $v + } + puts stderr $d +} + +#'" fix poor old emacs font-lock mode + +proc _git_cmd {name} { + global _git_cmd_path + + if {[catch {set v $_git_cmd_path($name)}]} { + switch -- $name { + version - + --version - + --exec-path { return [list $::_git $name] } + } + + set p [gitexec git-$name$::_search_exe] + if {[file exists $p]} { + set v [list $p] + } elseif {[is_Windows] && [file exists [gitexec git-$name]]} { + # Try to determine what sort of magic will make + # git-$name go and do its thing, because native + # Tcl on Windows doesn't know it. + # + set p [gitexec git-$name] + set f [open $p r] + set s [gets $f] + close $f + + switch -glob -- [lindex $s 0] { + #!*sh { set i sh } + #!*perl { set i perl } + #!*python { set i python } + default { error "git-$name is not supported: $s" } + } + + upvar #0 _$i interp + if {![info exists interp]} { + set interp [_which $i] + } + if {$interp eq {}} { + error "git-$name requires $i (not in PATH)" + } + set v [concat [list $interp] [lrange $s 1 end] [list $p]] + } else { + # Assume it is builtin to git somehow and we + # aren't actually able to see a file for it. + # + set v [list $::_git $name] + } + set _git_cmd_path($name) $v + } + return $v +} + +proc _which {what args} { + global env _search_exe _search_path + + if {$_search_path eq {}} { + if {[is_Cygwin] && [regexp {^(/|\.:)} $env(PATH)]} { + set _search_path [split [exec cygpath \ + --windows \ + --path \ + --absolute \ + $env(PATH)] {;}] + set _search_exe .exe + } elseif {[is_Windows]} { + set gitguidir [file dirname [info script]] + regsub -all ";" $gitguidir "\\;" gitguidir + set env(PATH) "$gitguidir;$env(PATH)" + set _search_path [split $env(PATH) {;}] + set _search_exe .exe + } else { + set _search_path [split $env(PATH) :] + set _search_exe {} + } + } + + if {[is_Windows] && [lsearch -exact $args -script] >= 0} { + set suffix {} + } else { + set suffix $_search_exe + } + + foreach p $_search_path { + set p [file join $p $what$suffix] + if {[file exists $p]} { + return [file normalize $p] + } + } + return {} +} + +# Test a file for a hashbang to identify executable scripts on Windows. +proc is_shellscript {filename} { + if {![file exists $filename]} {return 0} + set f [open $filename r] + fconfigure $f -encoding binary + set magic [read $f 2] + close $f + return [expr {$magic eq "#!"}] +} + +# Run a command connected via pipes on stdout. +# This is for use with textconv filters and uses sh -c "..." to allow it to +# contain a command with arguments. On windows we must check for shell +# scripts specifically otherwise just call the filter command. +proc open_cmd_pipe {cmd path} { + global env + if {![file executable [shellpath]]} { + set exe [auto_execok [lindex $cmd 0]] + if {[is_shellscript [lindex $exe 0]]} { + set run [linsert [auto_execok sh] end -c "$cmd \"\$0\"" $path] + } else { + set run [concat $exe [lrange $cmd 1 end] $path] + } + } else { + set run [list [shellpath] -c "$cmd \"\$0\"" $path] + } + return [open |$run r] +} + +proc _lappend_nice {cmd_var} { + global _nice + upvar $cmd_var cmd + + if {![info exists _nice]} { + set _nice [_which nice] + if {[catch {exec $_nice git version}]} { + set _nice {} + } elseif {[is_Windows] && [file dirname $_nice] ne [file dirname $::_git]} { + set _nice {} + } + } + if {$_nice ne {}} { + lappend cmd $_nice + } +} + +proc git {args} { + set fd [eval [list git_read] $args] + fconfigure $fd -translation binary -encoding utf-8 + set result [string trimright [read $fd] "\n"] + close $fd + if {$::_trace} { + puts stderr "< $result" + } + return $result +} + +proc _open_stdout_stderr {cmd} { + _trace_exec $cmd + if {[catch { + set fd [open [concat [list | ] $cmd] r] + } err]} { + if { [lindex $cmd end] eq {2>@1} + && $err eq {can not find channel named "1"} + } { + # Older versions of Tcl 8.4 don't have this 2>@1 IO + # redirect operator. Fallback to |& cat for those. + # The command was not actually started, so its safe + # to try to start it a second time. + # + set fd [open [concat \ + [list | ] \ + [lrange $cmd 0 end-1] \ + [list |& cat] \ + ] r] + } else { + error $err + } + } + fconfigure $fd -eofchar {} + return $fd +} + +proc git_read {args} { + set opt [list] + + while {1} { + switch -- [lindex $args 0] { + --nice { + _lappend_nice opt + } + + --stderr { + lappend args 2>@1 + } + + default { + break + } + + } + + set args [lrange $args 1 end] + } + + set cmdp [_git_cmd [lindex $args 0]] + set args [lrange $args 1 end] + + return [_open_stdout_stderr [concat $opt $cmdp $args]] +} + +proc git_write {args} { + set opt [list] + + while {1} { + switch -- [lindex $args 0] { + --nice { + _lappend_nice opt + } + + default { + break + } + + } + + set args [lrange $args 1 end] + } + + set cmdp [_git_cmd [lindex $args 0]] + set args [lrange $args 1 end] + + _trace_exec [concat $opt $cmdp $args] + return [open [concat [list | ] $opt $cmdp $args] w] +} + +proc githook_read {hook_name args} { + set pchook [gitdir hooks $hook_name] + lappend args 2>@1 + + # On Windows [file executable] might lie so we need to ask + # the shell if the hook is executable. Yes that's annoying. + # + if {[is_Windows]} { + upvar #0 _sh interp + if {![info exists interp]} { + set interp [_which sh] + } + if {$interp eq {}} { + error "hook execution requires sh (not in PATH)" + } + + set scr {if test -x "$1";then exec "$@";fi} + set sh_c [list $interp -c $scr $interp $pchook] + return [_open_stdout_stderr [concat $sh_c $args]] + } + + if {[file executable $pchook]} { + return [_open_stdout_stderr [concat [list $pchook] $args]] + } + + return {} +} + +proc kill_file_process {fd} { + set process [pid $fd] + + catch { + if {[is_Windows]} { + exec taskkill /pid $process + } else { + exec kill $process + } + } +} + +proc gitattr {path attr default} { + if {[catch {set r [git check-attr $attr -- $path]}]} { + set r unspecified + } else { + set r [join [lrange [split $r :] 2 end] :] + regsub {^ } $r {} r + } + if {$r eq {unspecified}} { + return $default + } + return $r +} + +proc sq {value} { + regsub -all ' $value "'\\''" value + return "'$value'" +} + +proc load_current_branch {} { + global current_branch is_detached + + set fd [open [gitdir HEAD] r] + fconfigure $fd -translation binary -encoding utf-8 + if {[gets $fd ref] < 1} { + set ref {} + } + close $fd + + set pfx {ref: refs/heads/} + set len [string length $pfx] + if {[string equal -length $len $pfx $ref]} { + # We're on a branch. It might not exist. But + # HEAD looks good enough to be a branch. + # + set current_branch [string range $ref $len end] + set is_detached 0 + } else { + # Assume this is a detached head. + # + set current_branch HEAD + set is_detached 1 + } +} + +auto_load tk_optionMenu +rename tk_optionMenu real__tkOptionMenu +proc tk_optionMenu {w varName args} { + set m [eval real__tkOptionMenu $w $varName $args] + $m configure -font font_ui + $w configure -font font_ui + return $m +} + +proc rmsel_tag {text} { + $text tag conf sel \ + -background [$text cget -background] \ + -foreground [$text cget -foreground] \ + -borderwidth 0 + $text tag conf in_sel -background lightgray + bind $text <Motion> break + return $text +} + +wm withdraw . +set root_exists 0 +bind . <Visibility> { + bind . <Visibility> {} + set root_exists 1 +} + +if {[is_Windows]} { + wm iconbitmap . -default $oguilib/git-gui.ico + set ::tk::AlwaysShowSelection 1 + bind . <Control-F2> {console show} + + # Spoof an X11 display for SSH + if {![info exists env(DISPLAY)]} { + set env(DISPLAY) :9999 + } +} else { + catch { + image create photo gitlogo -width 16 -height 16 + + gitlogo put #33CC33 -to 7 0 9 2 + gitlogo put #33CC33 -to 4 2 12 4 + gitlogo put #33CC33 -to 7 4 9 6 + gitlogo put #CC3333 -to 4 6 12 8 + gitlogo put gray26 -to 4 9 6 10 + gitlogo put gray26 -to 3 10 6 12 + gitlogo put gray26 -to 8 9 13 11 + gitlogo put gray26 -to 8 11 10 12 + gitlogo put gray26 -to 11 11 13 14 + gitlogo put gray26 -to 3 12 5 14 + gitlogo put gray26 -to 5 13 + gitlogo put gray26 -to 10 13 + gitlogo put gray26 -to 4 14 12 15 + gitlogo put gray26 -to 5 15 11 16 + gitlogo redither + + image create photo gitlogo32 -width 32 -height 32 + gitlogo32 copy gitlogo -zoom 2 2 + + wm iconphoto . -default gitlogo gitlogo32 + } +} + +###################################################################### +## +## config defaults + +set cursor_ptr arrow +font create font_ui +if {[lsearch -exact [font names] TkDefaultFont] != -1} { + eval [linsert [font actual TkDefaultFont] 0 font configure font_ui] + eval [linsert [font actual TkFixedFont] 0 font create font_diff] +} else { + font create font_diff -family Courier -size 10 + catch { + label .dummy + eval font configure font_ui [font actual [.dummy cget -font]] + destroy .dummy + } +} + +font create font_uiitalic +font create font_uibold +font create font_diffbold +font create font_diffitalic + +foreach class {Button Checkbutton Entry Label + Labelframe Listbox Message + Radiobutton Spinbox Text} { + option add *$class.font font_ui +} +if {![is_MacOSX]} { + option add *Menu.font font_ui + option add *Entry.borderWidth 1 startupFile + option add *Entry.relief sunken startupFile + option add *RadioButton.anchor w startupFile +} +unset class + +if {[is_Windows] || [is_MacOSX]} { + option add *Menu.tearOff 0 +} + +if {[is_MacOSX]} { + set M1B M1 + set M1T Cmd +} else { + set M1B Control + set M1T Ctrl +} + +proc bind_button3 {w cmd} { + bind $w <Any-Button-3> $cmd + if {[is_MacOSX]} { + # Mac OS X sends Button-2 on right click through three-button mouse, + # or through trackpad right-clicking (two-finger touch + click). + bind $w <Any-Button-2> $cmd + bind $w <Control-Button-1> $cmd + } +} + +proc apply_config {} { + global repo_config font_descs + + foreach option $font_descs { + set name [lindex $option 0] + set font [lindex $option 1] + if {[catch { + set need_weight 1 + foreach {cn cv} $repo_config(gui.$name) { + if {$cn eq {-weight}} { + set need_weight 0 + } + font configure $font $cn $cv + } + if {$need_weight} { + font configure $font -weight normal + } + } err]} { + error_popup [strcat [mc "Invalid font specified in %s:" "gui.$name"] "\n\n$err"] + } + foreach {cn cv} [font configure $font] { + font configure ${font}bold $cn $cv + font configure ${font}italic $cn $cv + } + font configure ${font}bold -weight bold + font configure ${font}italic -slant italic + } + + global use_ttk NS + set use_ttk 0 + set NS {} + if {$repo_config(gui.usettk)} { + set use_ttk [package vsatisfies [package provide Tk] 8.5] + if {$use_ttk} { + set NS ttk + bind [winfo class .] <<ThemeChanged>> [list InitTheme] + pave_toplevel . + } + } +} + +set default_config(branch.autosetupmerge) true +set default_config(merge.tool) {} +set default_config(mergetool.keepbackup) true +set default_config(merge.diffstat) true +set default_config(merge.summary) false +set default_config(merge.verbosity) 2 +set default_config(user.name) {} +set default_config(user.email) {} + +set default_config(gui.encoding) [encoding system] +set default_config(gui.matchtrackingbranch) false +set default_config(gui.textconv) true +set default_config(gui.pruneduringfetch) false +set default_config(gui.trustmtime) false +set default_config(gui.fastcopyblame) false +set default_config(gui.maxrecentrepo) 10 +set default_config(gui.copyblamethreshold) 40 +set default_config(gui.blamehistoryctx) 7 +set default_config(gui.diffcontext) 5 +set default_config(gui.diffopts) {} +set default_config(gui.commitmsgwidth) 75 +set default_config(gui.newbranchtemplate) {} +set default_config(gui.spellingdictionary) {} +set default_config(gui.fontui) [font configure font_ui] +set default_config(gui.fontdiff) [font configure font_diff] +# TODO: this option should be added to the git-config documentation +set default_config(gui.maxfilesdisplayed) 5000 +set default_config(gui.usettk) 1 +set default_config(gui.warndetachedcommit) 1 +set default_config(gui.tabsize) 8 +set font_descs { + {fontui font_ui {mc "Main Font"}} + {fontdiff font_diff {mc "Diff/Console Font"}} +} +set default_config(gui.stageuntracked) ask +set default_config(gui.displayuntracked) true + +###################################################################### +## +## find git + +set _git [_which git] +if {$_git eq {}} { + catch {wm withdraw .} + tk_messageBox \ + -icon error \ + -type ok \ + -title [mc "git-gui: fatal error"] \ + -message [mc "Cannot find git in PATH."] + exit 1 +} + +###################################################################### +## +## version check + +if {[catch {set _git_version [git --version]} err]} { + catch {wm withdraw .} + tk_messageBox \ + -icon error \ + -type ok \ + -title [mc "git-gui: fatal error"] \ + -message "Cannot determine Git version: + +$err + +[appname] requires Git 1.5.0 or later." + exit 1 +} +if {![regsub {^git version } $_git_version {} _git_version]} { + catch {wm withdraw .} + tk_messageBox \ + -icon error \ + -type ok \ + -title [mc "git-gui: fatal error"] \ + -message [strcat [mc "Cannot parse Git version string:"] "\n\n$_git_version"] + exit 1 +} + +proc get_trimmed_version {s} { + set r {} + foreach x [split $s -._] { + if {[string is integer -strict $x]} { + lappend r $x + } else { + break + } + } + return [join $r .] +} +set _real_git_version $_git_version +set _git_version [get_trimmed_version $_git_version] + +if {![regexp {^[1-9]+(\.[0-9]+)+$} $_git_version]} { + catch {wm withdraw .} + if {[tk_messageBox \ + -icon warning \ + -type yesno \ + -default no \ + -title "[appname]: warning" \ + -message [mc "Git version cannot be determined. + +%s claims it is version '%s'. + +%s requires at least Git 1.5.0 or later. + +Assume '%s' is version 1.5.0? +" $_git $_real_git_version [appname] $_real_git_version]] eq {yes}} { + set _git_version 1.5.0 + } else { + exit 1 + } +} +unset _real_git_version + +proc git-version {args} { + global _git_version + + switch [llength $args] { + 0 { + return $_git_version + } + + 2 { + set op [lindex $args 0] + set vr [lindex $args 1] + set cm [package vcompare $_git_version $vr] + return [expr $cm $op 0] + } + + 4 { + set type [lindex $args 0] + set name [lindex $args 1] + set parm [lindex $args 2] + set body [lindex $args 3] + + if {($type ne {proc} && $type ne {method})} { + error "Invalid arguments to git-version" + } + if {[llength $body] < 2 || [lindex $body end-1] ne {default}} { + error "Last arm of $type $name must be default" + } + + foreach {op vr cb} [lrange $body 0 end-2] { + if {[git-version $op $vr]} { + return [uplevel [list $type $name $parm $cb]] + } + } + + return [uplevel [list $type $name $parm [lindex $body end]]] + } + + default { + error "git-version >= x" + } + + } +} + +if {[git-version < 1.5]} { + catch {wm withdraw .} + tk_messageBox \ + -icon error \ + -type ok \ + -title [mc "git-gui: fatal error"] \ + -message "[appname] requires Git 1.5.0 or later. + +You are using [git-version]: + +[git --version]" + exit 1 +} + +###################################################################### +## +## configure our library + +set idx [file join $oguilib tclIndex] +if {[catch {set fd [open $idx r]} err]} { + catch {wm withdraw .} + tk_messageBox \ + -icon error \ + -type ok \ + -title [mc "git-gui: fatal error"] \ + -message $err + exit 1 +} +if {[gets $fd] eq {# Autogenerated by git-gui Makefile}} { + set idx [list] + while {[gets $fd n] >= 0} { + if {$n ne {} && ![string match #* $n]} { + lappend idx $n + } + } +} else { + set idx {} +} +close $fd + +if {$idx ne {}} { + set loaded [list] + foreach p $idx { + if {[lsearch -exact $loaded $p] >= 0} continue + source [file join $oguilib $p] + lappend loaded $p + } + unset loaded p +} else { + set auto_path [concat [list $oguilib] $auto_path] +} +unset -nocomplain idx fd + +###################################################################### +## +## config file parsing + +git-version proc _parse_config {arr_name args} { + >= 1.5.3 { + upvar $arr_name arr + array unset arr + set buf {} + catch { + set fd_rc [eval \ + [list git_read config] \ + $args \ + [list --null --list]] + fconfigure $fd_rc -translation binary -encoding utf-8 + set buf [read $fd_rc] + close $fd_rc + } + foreach line [split $buf "\0"] { + if {[regexp {^([^\n]+)\n(.*)$} $line line name value]} { + if {[is_many_config $name]} { + lappend arr($name) $value + } else { + set arr($name) $value + } + } elseif {[regexp {^([^\n]+)$} $line line name]} { + # no value given, but interpreting them as + # boolean will be handled as true + set arr($name) {} + } + } + } + default { + upvar $arr_name arr + array unset arr + catch { + set fd_rc [eval [list git_read config --list] $args] + while {[gets $fd_rc line] >= 0} { + if {[regexp {^([^=]+)=(.*)$} $line line name value]} { + if {[is_many_config $name]} { + lappend arr($name) $value + } else { + set arr($name) $value + } + } elseif {[regexp {^([^=]+)$} $line line name]} { + # no value given, but interpreting them as + # boolean will be handled as true + set arr($name) {} + } + } + close $fd_rc + } + } +} + +proc load_config {include_global} { + global repo_config global_config system_config default_config + + if {$include_global} { + _parse_config system_config --system + _parse_config global_config --global + } + _parse_config repo_config + + foreach name [array names default_config] { + if {[catch {set v $system_config($name)}]} { + set system_config($name) $default_config($name) + } + } + foreach name [array names system_config] { + if {[catch {set v $global_config($name)}]} { + set global_config($name) $system_config($name) + } + if {[catch {set v $repo_config($name)}]} { + set repo_config($name) $system_config($name) + } + } +} + +###################################################################### +## +## feature option selection + +if {[regexp {^git-(.+)$} [file tail $argv0] _junk subcommand]} { + unset _junk +} else { + set subcommand gui +} +if {$subcommand eq {gui.sh}} { + set subcommand gui +} +if {$subcommand eq {gui} && [llength $argv] > 0} { + set subcommand [lindex $argv 0] + set argv [lrange $argv 1 end] +} + +enable_option multicommit +enable_option branch +enable_option transport +disable_option bare + +switch -- $subcommand { +browser - +blame { + enable_option bare + + disable_option multicommit + disable_option branch + disable_option transport +} +citool { + enable_option singlecommit + enable_option retcode + + disable_option multicommit + disable_option branch + disable_option transport + + while {[llength $argv] > 0} { + set a [lindex $argv 0] + switch -- $a { + --amend { + enable_option initialamend + } + --nocommit { + enable_option nocommit + enable_option nocommitmsg + } + --commitmsg { + disable_option nocommitmsg + } + default { + break + } + } + + set argv [lrange $argv 1 end] + } +} +} + +###################################################################### +## +## execution environment + +set have_tk85 [expr {[package vcompare $tk_version "8.5"] >= 0}] + +# Suggest our implementation of askpass, if none is set +if {![info exists env(SSH_ASKPASS)]} { + set env(SSH_ASKPASS) [gitexec git-gui--askpass] +} + +###################################################################### +## +## repository setup + +set picked 0 +if {[catch { + set _gitdir $env(GIT_DIR) + set _prefix {} + }] + && [catch { + # beware that from the .git dir this sets _gitdir to . + # and _prefix to the empty string + set _gitdir [git rev-parse --git-dir] + set _prefix [git rev-parse --show-prefix] + } err]} { + load_config 1 + apply_config + choose_repository::pick + set picked 1 +} + +# we expand the _gitdir when it's just a single dot (i.e. when we're being +# run from the .git dir itself) lest the routines to find the worktree +# get confused +if {$_gitdir eq "."} { + set _gitdir [pwd] +} + +if {![file isdirectory $_gitdir] && [is_Cygwin]} { + catch {set _gitdir [exec cygpath --windows $_gitdir]} +} +if {![file isdirectory $_gitdir]} { + catch {wm withdraw .} + error_popup [strcat [mc "Git directory not found:"] "\n\n$_gitdir"] + exit 1 +} +# _gitdir exists, so try loading the config +load_config 0 +apply_config + +# v1.7.0 introduced --show-toplevel to return the canonical work-tree +if {[package vcompare $_git_version 1.7.0] >= 0} { + if { [is_Cygwin] } { + catch {set _gitworktree [exec cygpath --windows [git rev-parse --show-toplevel]]} + } else { + set _gitworktree [git rev-parse --show-toplevel] + } +} else { + # try to set work tree from environment, core.worktree or use + # cdup to obtain a relative path to the top of the worktree. If + # run from the top, the ./ prefix ensures normalize expands pwd. + if {[catch { set _gitworktree $env(GIT_WORK_TREE) }]} { + set _gitworktree [get_config core.worktree] + if {$_gitworktree eq ""} { + set _gitworktree [file normalize ./[git rev-parse --show-cdup]] + } + } +} + +if {$_prefix ne {}} { + if {$_gitworktree eq {}} { + regsub -all {[^/]+/} $_prefix ../ cdup + } else { + set cdup $_gitworktree + } + if {[catch {cd $cdup} err]} { + catch {wm withdraw .} + error_popup [strcat [mc "Cannot move to top of working directory:"] "\n\n$err"] + exit 1 + } + set _gitworktree [pwd] + unset cdup +} elseif {![is_enabled bare]} { + if {[is_bare]} { + catch {wm withdraw .} + error_popup [strcat [mc "Cannot use bare repository:"] "\n\n$_gitdir"] + exit 1 + } + if {$_gitworktree eq {}} { + set _gitworktree [file dirname $_gitdir] + } + if {[catch {cd $_gitworktree} err]} { + catch {wm withdraw .} + error_popup [strcat [mc "No working directory"] " $_gitworktree:\n\n$err"] + exit 1 + } + set _gitworktree [pwd] +} +set _reponame [file split [file normalize $_gitdir]] +if {[lindex $_reponame end] eq {.git}} { + set _reponame [lindex $_reponame end-1] +} else { + set _reponame [lindex $_reponame end] +} + +set env(GIT_DIR) $_gitdir +set env(GIT_WORK_TREE) $_gitworktree + +###################################################################### +## +## global init + +set current_diff_path {} +set current_diff_side {} +set diff_actions [list] + +set HEAD {} +set PARENT {} +set MERGE_HEAD [list] +set commit_type {} +set commit_type_is_amend 0 +set empty_tree {} +set current_branch {} +set is_detached 0 +set current_diff_path {} +set is_3way_diff 0 +set is_submodule_diff 0 +set is_conflict_diff 0 +set diff_empty_count 0 +set last_revert {} +set last_revert_enc {} + +set nullid "0000000000000000000000000000000000000000" +set nullid2 "0000000000000000000000000000000000000001" + +###################################################################### +## +## task management + +set rescan_active 0 +set diff_active 0 +set last_clicked {} + +set disable_on_lock [list] +set index_lock_type none + +proc lock_index {type} { + global index_lock_type disable_on_lock + + if {$index_lock_type eq {none}} { + set index_lock_type $type + foreach w $disable_on_lock { + uplevel #0 $w disabled + } + return 1 + } elseif {$index_lock_type eq "begin-$type"} { + set index_lock_type $type + return 1 + } + return 0 +} + +proc unlock_index {} { + global index_lock_type disable_on_lock + + set index_lock_type none + foreach w $disable_on_lock { + uplevel #0 $w normal + } +} + +###################################################################### +## +## status + +proc repository_state {ctvar hdvar mhvar} { + global current_branch + upvar $ctvar ct $hdvar hd $mhvar mh + + set mh [list] + + load_current_branch + if {[catch {set hd [git rev-parse --verify HEAD]}]} { + set hd {} + set ct initial + return + } + + set merge_head [gitdir MERGE_HEAD] + if {[file exists $merge_head]} { + set ct merge + set fd_mh [open $merge_head r] + while {[gets $fd_mh line] >= 0} { + lappend mh $line + } + close $fd_mh + return + } + + set ct normal +} + +proc PARENT {} { + global PARENT empty_tree + + set p [lindex $PARENT 0] + if {$p ne {}} { + return $p + } + if {$empty_tree eq {}} { + set empty_tree [git mktree << {}] + } + return $empty_tree +} + +proc force_amend {} { + global commit_type_is_amend + global HEAD PARENT MERGE_HEAD commit_type + + repository_state newType newHEAD newMERGE_HEAD + set HEAD $newHEAD + set PARENT $newHEAD + set MERGE_HEAD $newMERGE_HEAD + set commit_type $newType + + set commit_type_is_amend 1 + do_select_commit_type +} + +proc rescan {after {honor_trustmtime 1}} { + global HEAD PARENT MERGE_HEAD commit_type + global ui_index ui_workdir ui_comm + global rescan_active file_states + global repo_config + + if {$rescan_active > 0 || ![lock_index read]} return + + repository_state newType newHEAD newMERGE_HEAD + if {[string match amend* $commit_type] + && $newType eq {normal} + && $newHEAD eq $HEAD} { + } else { + set HEAD $newHEAD + set PARENT $newHEAD + set MERGE_HEAD $newMERGE_HEAD + set commit_type $newType + } + + array unset file_states + + if {!$::GITGUI_BCK_exists && + (![$ui_comm edit modified] + || [string trim [$ui_comm get 0.0 end]] eq {})} { + if {[string match amend* $commit_type]} { + } elseif {[load_message GITGUI_MSG utf-8]} { + } elseif {[run_prepare_commit_msg_hook]} { + } elseif {[load_message MERGE_MSG]} { + } elseif {[load_message SQUASH_MSG]} { + } + $ui_comm edit reset + $ui_comm edit modified false + } + + if {$honor_trustmtime && $repo_config(gui.trustmtime) eq {true}} { + rescan_stage2 {} $after + } else { + set rescan_active 1 + ui_status [mc "Refreshing file status..."] + set fd_rf [git_read update-index \ + -q \ + --unmerged \ + --ignore-missing \ + --refresh \ + ] + fconfigure $fd_rf -blocking 0 -translation binary + fileevent $fd_rf readable \ + [list rescan_stage2 $fd_rf $after] + } +} + +if {[is_Cygwin]} { + set is_git_info_exclude {} + proc have_info_exclude {} { + global is_git_info_exclude + + if {$is_git_info_exclude eq {}} { + if {[catch {exec test -f [gitdir info exclude]}]} { + set is_git_info_exclude 0 + } else { + set is_git_info_exclude 1 + } + } + return $is_git_info_exclude + } +} else { + proc have_info_exclude {} { + return [file readable [gitdir info exclude]] + } +} + +proc rescan_stage2 {fd after} { + global rescan_active buf_rdi buf_rdf buf_rlo + + if {$fd ne {}} { + read $fd + if {![eof $fd]} return + close $fd + } + + if {[package vcompare $::_git_version 1.6.3] >= 0} { + set ls_others [list --exclude-standard] + } else { + set ls_others [list --exclude-per-directory=.gitignore] + if {[have_info_exclude]} { + lappend ls_others "--exclude-from=[gitdir info exclude]" + } + set user_exclude [get_config core.excludesfile] + if {$user_exclude ne {} && [file readable $user_exclude]} { + lappend ls_others "--exclude-from=[file normalize $user_exclude]" + } + } + + set buf_rdi {} + set buf_rdf {} + set buf_rlo {} + + set rescan_active 2 + ui_status [mc "Scanning for modified files ..."] + if {[git-version >= "1.7.2"]} { + set fd_di [git_read diff-index --cached --ignore-submodules=dirty -z [PARENT]] + } else { + set fd_di [git_read diff-index --cached -z [PARENT]] + } + set fd_df [git_read diff-files -z] + + fconfigure $fd_di -blocking 0 -translation binary -encoding binary + fconfigure $fd_df -blocking 0 -translation binary -encoding binary + + fileevent $fd_di readable [list read_diff_index $fd_di $after] + fileevent $fd_df readable [list read_diff_files $fd_df $after] + + if {[is_config_true gui.displayuntracked]} { + set fd_lo [eval git_read ls-files --others -z $ls_others] + fconfigure $fd_lo -blocking 0 -translation binary -encoding binary + fileevent $fd_lo readable [list read_ls_others $fd_lo $after] + incr rescan_active + } +} + +proc load_message {file {encoding {}}} { + global ui_comm + + set f [gitdir $file] + if {[file isfile $f]} { + if {[catch {set fd [open $f r]}]} { + return 0 + } + fconfigure $fd -eofchar {} + if {$encoding ne {}} { + fconfigure $fd -encoding $encoding + } + set content [string trim [read $fd]] + close $fd + regsub -all -line {[ \r\t]+$} $content {} content + $ui_comm delete 0.0 end + $ui_comm insert end $content + return 1 + } + return 0 +} + +proc run_prepare_commit_msg_hook {} { + global pch_error + + # prepare-commit-msg requires PREPARE_COMMIT_MSG exist. From git-gui + # it will be .git/MERGE_MSG (merge), .git/SQUASH_MSG (squash), or an + # empty file but existent file. + + set fd_pcm [open [gitdir PREPARE_COMMIT_MSG] a] + + if {[file isfile [gitdir MERGE_MSG]]} { + set pcm_source "merge" + set fd_mm [open [gitdir MERGE_MSG] r] + fconfigure $fd_mm -encoding utf-8 + puts -nonewline $fd_pcm [read $fd_mm] + close $fd_mm + } elseif {[file isfile [gitdir SQUASH_MSG]]} { + set pcm_source "squash" + set fd_sm [open [gitdir SQUASH_MSG] r] + fconfigure $fd_sm -encoding utf-8 + puts -nonewline $fd_pcm [read $fd_sm] + close $fd_sm + } else { + set pcm_source "" + } + + close $fd_pcm + + set fd_ph [githook_read prepare-commit-msg \ + [gitdir PREPARE_COMMIT_MSG] $pcm_source] + if {$fd_ph eq {}} { + catch {file delete [gitdir PREPARE_COMMIT_MSG]} + return 0; + } + + ui_status [mc "Calling prepare-commit-msg hook..."] + set pch_error {} + + fconfigure $fd_ph -blocking 0 -translation binary -eofchar {} + fileevent $fd_ph readable \ + [list prepare_commit_msg_hook_wait $fd_ph] + + return 1; +} + +proc prepare_commit_msg_hook_wait {fd_ph} { + global pch_error + + append pch_error [read $fd_ph] + fconfigure $fd_ph -blocking 1 + if {[eof $fd_ph]} { + if {[catch {close $fd_ph}]} { + ui_status [mc "Commit declined by prepare-commit-msg hook."] + hook_failed_popup prepare-commit-msg $pch_error + catch {file delete [gitdir PREPARE_COMMIT_MSG]} + exit 1 + } else { + load_message PREPARE_COMMIT_MSG + } + set pch_error {} + catch {file delete [gitdir PREPARE_COMMIT_MSG]} + return + } + fconfigure $fd_ph -blocking 0 + catch {file delete [gitdir PREPARE_COMMIT_MSG]} +} + +proc read_diff_index {fd after} { + global buf_rdi + + append buf_rdi [read $fd] + set c 0 + set n [string length $buf_rdi] + while {$c < $n} { + set z1 [string first "\0" $buf_rdi $c] + if {$z1 == -1} break + incr z1 + set z2 [string first "\0" $buf_rdi $z1] + if {$z2 == -1} break + + incr c + set i [split [string range $buf_rdi $c [expr {$z1 - 2}]] { }] + set p [string range $buf_rdi $z1 [expr {$z2 - 1}]] + merge_state \ + [encoding convertfrom utf-8 $p] \ + [lindex $i 4]? \ + [list [lindex $i 0] [lindex $i 2]] \ + [list] + set c $z2 + incr c + } + if {$c < $n} { + set buf_rdi [string range $buf_rdi $c end] + } else { + set buf_rdi {} + } + + rescan_done $fd buf_rdi $after +} + +proc read_diff_files {fd after} { + global buf_rdf + + append buf_rdf [read $fd] + set c 0 + set n [string length $buf_rdf] + while {$c < $n} { + set z1 [string first "\0" $buf_rdf $c] + if {$z1 == -1} break + incr z1 + set z2 [string first "\0" $buf_rdf $z1] + if {$z2 == -1} break + + incr c + set i [split [string range $buf_rdf $c [expr {$z1 - 2}]] { }] + set p [string range $buf_rdf $z1 [expr {$z2 - 1}]] + merge_state \ + [encoding convertfrom utf-8 $p] \ + ?[lindex $i 4] \ + [list] \ + [list [lindex $i 0] [lindex $i 2]] + set c $z2 + incr c + } + if {$c < $n} { + set buf_rdf [string range $buf_rdf $c end] + } else { + set buf_rdf {} + } + + rescan_done $fd buf_rdf $after +} + +proc read_ls_others {fd after} { + global buf_rlo + + append buf_rlo [read $fd] + set pck [split $buf_rlo "\0"] + set buf_rlo [lindex $pck end] + foreach p [lrange $pck 0 end-1] { + set p [encoding convertfrom utf-8 $p] + if {[string index $p end] eq {/}} { + set p [string range $p 0 end-1] + } + merge_state $p ?O + } + rescan_done $fd buf_rlo $after +} + +proc rescan_done {fd buf after} { + global rescan_active current_diff_path + global file_states repo_config + upvar $buf to_clear + + if {![eof $fd]} return + set to_clear {} + close $fd + if {[incr rescan_active -1] > 0} return + + prune_selection + unlock_index + display_all_files + if {$current_diff_path ne {}} { reshow_diff $after } + if {$current_diff_path eq {}} { select_first_diff $after } +} + +proc prune_selection {} { + global file_states selected_paths + + foreach path [array names selected_paths] { + if {[catch {set still_here $file_states($path)}]} { + unset selected_paths($path) + } + } +} + +###################################################################### +## +## ui helpers + +proc mapicon {w state path} { + global all_icons + + if {[catch {set r $all_icons($state$w)}]} { + puts "error: no icon for $w state={$state} $path" + return file_plain + } + return $r +} + +proc mapdesc {state path} { + global all_descs + + if {[catch {set r $all_descs($state)}]} { + puts "error: no desc for state={$state} $path" + return $state + } + return $r +} + +proc ui_status {msg} { + global main_status + if {[info exists main_status]} { + $main_status show $msg + } +} + +proc ui_ready {} { + global main_status + if {[info exists main_status]} { + $main_status show [mc "Ready."] + } +} + +proc escape_path {path} { + regsub -all {\\} $path "\\\\" path + regsub -all "\n" $path "\\n" path + return $path +} + +proc short_path {path} { + return [escape_path [lindex [file split $path] end]] +} + +set next_icon_id 0 +set null_sha1 [string repeat 0 40] + +proc merge_state {path new_state {head_info {}} {index_info {}}} { + global file_states next_icon_id null_sha1 + + set s0 [string index $new_state 0] + set s1 [string index $new_state 1] + + if {[catch {set info $file_states($path)}]} { + set state __ + set icon n[incr next_icon_id] + } else { + set state [lindex $info 0] + set icon [lindex $info 1] + if {$head_info eq {}} {set head_info [lindex $info 2]} + if {$index_info eq {}} {set index_info [lindex $info 3]} + } + + if {$s0 eq {?}} {set s0 [string index $state 0]} \ + elseif {$s0 eq {_}} {set s0 _} + + if {$s1 eq {?}} {set s1 [string index $state 1]} \ + elseif {$s1 eq {_}} {set s1 _} + + if {$s0 eq {A} && $s1 eq {_} && $head_info eq {}} { + set head_info [list 0 $null_sha1] + } elseif {$s0 ne {_} && [string index $state 0] eq {_} + && $head_info eq {}} { + set head_info $index_info + } elseif {$s0 eq {_} && [string index $state 0] ne {_}} { + set index_info $head_info + set head_info {} + } + + set file_states($path) [list $s0$s1 $icon \ + $head_info $index_info \ + ] + return $state +} + +proc display_file_helper {w path icon_name old_m new_m} { + global file_lists + + if {$new_m eq {_}} { + set lno [lsearch -sorted -exact $file_lists($w) $path] + if {$lno >= 0} { + set file_lists($w) [lreplace $file_lists($w) $lno $lno] + incr lno + $w conf -state normal + $w delete $lno.0 [expr {$lno + 1}].0 + $w conf -state disabled + } + } elseif {$old_m eq {_} && $new_m ne {_}} { + lappend file_lists($w) $path + set file_lists($w) [lsort -unique $file_lists($w)] + set lno [lsearch -sorted -exact $file_lists($w) $path] + incr lno + $w conf -state normal + $w image create $lno.0 \ + -align center -padx 5 -pady 1 \ + -name $icon_name \ + -image [mapicon $w $new_m $path] + $w insert $lno.1 "[escape_path $path]\n" + $w conf -state disabled + } elseif {$old_m ne $new_m} { + $w conf -state normal + $w image conf $icon_name -image [mapicon $w $new_m $path] + $w conf -state disabled + } +} + +proc display_file {path state} { + global file_states selected_paths + global ui_index ui_workdir + + set old_m [merge_state $path $state] + set s $file_states($path) + set new_m [lindex $s 0] + set icon_name [lindex $s 1] + + set o [string index $old_m 0] + set n [string index $new_m 0] + if {$o eq {U}} { + set o _ + } + if {$n eq {U}} { + set n _ + } + display_file_helper $ui_index $path $icon_name $o $n + + if {[string index $old_m 0] eq {U}} { + set o U + } else { + set o [string index $old_m 1] + } + if {[string index $new_m 0] eq {U}} { + set n U + } else { + set n [string index $new_m 1] + } + display_file_helper $ui_workdir $path $icon_name $o $n + + if {$new_m eq {__}} { + unset file_states($path) + catch {unset selected_paths($path)} + } +} + +proc display_all_files_helper {w path icon_name m} { + global file_lists + + lappend file_lists($w) $path + set lno [expr {[lindex [split [$w index end] .] 0] - 1}] + $w image create end \ + -align center -padx 5 -pady 1 \ + -name $icon_name \ + -image [mapicon $w $m $path] + $w insert end "[escape_path $path]\n" +} + +set files_warning 0 +proc display_all_files {} { + global ui_index ui_workdir + global file_states file_lists + global last_clicked + global files_warning + + $ui_index conf -state normal + $ui_workdir conf -state normal + + $ui_index delete 0.0 end + $ui_workdir delete 0.0 end + set last_clicked {} + + set file_lists($ui_index) [list] + set file_lists($ui_workdir) [list] + + set to_display [lsort [array names file_states]] + set display_limit [get_config gui.maxfilesdisplayed] + set displayed 0 + foreach path $to_display { + set s $file_states($path) + set m [lindex $s 0] + set icon_name [lindex $s 1] + + if {$displayed > $display_limit && [string index $m 1] eq {O} } { + if {!$files_warning} { + # do not repeatedly warn: + set files_warning 1 + info_popup [mc "Display limit (gui.maxfilesdisplayed = %s) reached, not showing all %s files." \ + $display_limit [llength $to_display]] + } + continue + } + + set s [string index $m 0] + if {$s ne {U} && $s ne {_}} { + display_all_files_helper $ui_index $path \ + $icon_name $s + } + + if {[string index $m 0] eq {U}} { + set s U + } else { + set s [string index $m 1] + } + if {$s ne {_}} { + display_all_files_helper $ui_workdir $path \ + $icon_name $s + incr displayed + } + } + + $ui_index conf -state disabled + $ui_workdir conf -state disabled +} + +###################################################################### +## +## icons + +set filemask { +#define mask_width 14 +#define mask_height 15 +static unsigned char mask_bits[] = { + 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, + 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f, + 0xfe, 0x1f, 0xfe, 0x1f, 0xfe, 0x1f}; +} + +image create bitmap file_plain -background white -foreground black -data { +#define plain_width 14 +#define plain_height 15 +static unsigned char plain_bits[] = { + 0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10, + 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, 0x02, 0x10, + 0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f}; +} -maskdata $filemask + +image create bitmap file_mod -background white -foreground blue -data { +#define mod_width 14 +#define mod_height 15 +static unsigned char mod_bits[] = { + 0xfe, 0x01, 0x02, 0x03, 0x7a, 0x05, 0x02, 0x09, 0x7a, 0x1f, 0x02, 0x10, + 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, + 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f}; +} -maskdata $filemask + +image create bitmap file_fulltick -background white -foreground "#007000" -data { +#define file_fulltick_width 14 +#define file_fulltick_height 15 +static unsigned char file_fulltick_bits[] = { + 0xfe, 0x01, 0x02, 0x1a, 0x02, 0x0c, 0x02, 0x0c, 0x02, 0x16, 0x02, 0x16, + 0x02, 0x13, 0x00, 0x13, 0x86, 0x11, 0x8c, 0x11, 0xd8, 0x10, 0xf2, 0x10, + 0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f}; +} -maskdata $filemask + +image create bitmap file_question -background white -foreground black -data { +#define file_question_width 14 +#define file_question_height 15 +static unsigned char file_question_bits[] = { + 0xfe, 0x01, 0x02, 0x02, 0xe2, 0x04, 0xf2, 0x09, 0x1a, 0x1b, 0x0a, 0x13, + 0x82, 0x11, 0xc2, 0x10, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, 0x62, 0x10, + 0x62, 0x10, 0x02, 0x10, 0xfe, 0x1f}; +} -maskdata $filemask + +image create bitmap file_removed -background white -foreground red -data { +#define file_removed_width 14 +#define file_removed_height 15 +static unsigned char file_removed_bits[] = { + 0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x02, 0x10, + 0x1a, 0x16, 0x32, 0x13, 0xe2, 0x11, 0xc2, 0x10, 0xe2, 0x11, 0x32, 0x13, + 0x1a, 0x16, 0x02, 0x10, 0xfe, 0x1f}; +} -maskdata $filemask + +image create bitmap file_merge -background white -foreground blue -data { +#define file_merge_width 14 +#define file_merge_height 15 +static unsigned char file_merge_bits[] = { + 0xfe, 0x01, 0x02, 0x03, 0x62, 0x05, 0x62, 0x09, 0x62, 0x1f, 0x62, 0x10, + 0xfa, 0x11, 0xf2, 0x10, 0x62, 0x10, 0x02, 0x10, 0xfa, 0x17, 0x02, 0x10, + 0xfa, 0x17, 0x02, 0x10, 0xfe, 0x1f}; +} -maskdata $filemask + +image create bitmap file_statechange -background white -foreground green -data { +#define file_statechange_width 14 +#define file_statechange_height 15 +static unsigned char file_statechange_bits[] = { + 0xfe, 0x01, 0x02, 0x03, 0x02, 0x05, 0x02, 0x09, 0x02, 0x1f, 0x62, 0x10, + 0x62, 0x10, 0xba, 0x11, 0xba, 0x11, 0x62, 0x10, 0x62, 0x10, 0x02, 0x10, + 0x02, 0x10, 0x02, 0x10, 0xfe, 0x1f}; +} -maskdata $filemask + +set ui_index .vpane.files.index.list +set ui_workdir .vpane.files.workdir.list + +set all_icons(_$ui_index) file_plain +set all_icons(A$ui_index) file_plain +set all_icons(M$ui_index) file_fulltick +set all_icons(D$ui_index) file_removed +set all_icons(U$ui_index) file_merge +set all_icons(T$ui_index) file_statechange + +set all_icons(_$ui_workdir) file_plain +set all_icons(M$ui_workdir) file_mod +set all_icons(D$ui_workdir) file_question +set all_icons(U$ui_workdir) file_merge +set all_icons(O$ui_workdir) file_plain +set all_icons(T$ui_workdir) file_statechange + +set max_status_desc 0 +foreach i { + {__ {mc "Unmodified"}} + + {_M {mc "Modified, not staged"}} + {M_ {mc "Staged for commit"}} + {MM {mc "Portions staged for commit"}} + {MD {mc "Staged for commit, missing"}} + + {_T {mc "File type changed, not staged"}} + {MT {mc "File type changed, old type staged for commit"}} + {AT {mc "File type changed, old type staged for commit"}} + {T_ {mc "File type changed, staged"}} + {TM {mc "File type change staged, modification not staged"}} + {TD {mc "File type change staged, file missing"}} + + {_O {mc "Untracked, not staged"}} + {A_ {mc "Staged for commit"}} + {AM {mc "Portions staged for commit"}} + {AD {mc "Staged for commit, missing"}} + + {_D {mc "Missing"}} + {D_ {mc "Staged for removal"}} + {DO {mc "Staged for removal, still present"}} + + {_U {mc "Requires merge resolution"}} + {U_ {mc "Requires merge resolution"}} + {UU {mc "Requires merge resolution"}} + {UM {mc "Requires merge resolution"}} + {UD {mc "Requires merge resolution"}} + {UT {mc "Requires merge resolution"}} + } { + set text [eval [lindex $i 1]] + if {$max_status_desc < [string length $text]} { + set max_status_desc [string length $text] + } + set all_descs([lindex $i 0]) $text +} +unset i + +###################################################################### +## +## util + +proc scrollbar2many {list mode args} { + foreach w $list {eval $w $mode $args} +} + +proc many2scrollbar {list mode sb top bottom} { + $sb set $top $bottom + foreach w $list {$w $mode moveto $top} +} + +proc incr_font_size {font {amt 1}} { + set sz [font configure $font -size] + incr sz $amt + font configure $font -size $sz + font configure ${font}bold -size $sz + font configure ${font}italic -size $sz +} + +###################################################################### +## +## ui commands + +proc do_gitk {revs {is_submodule false}} { + global current_diff_path file_states current_diff_side ui_index + global _gitdir _gitworktree + + # -- Always start gitk through whatever we were loaded with. This + # lets us bypass using shell process on Windows systems. + # + set exe [_which gitk -script] + set cmd [list [info nameofexecutable] $exe] + if {$exe eq {}} { + error_popup [mc "Couldn't find gitk in PATH"] + } else { + global env + + set pwd [pwd] + + if {!$is_submodule} { + if {![is_bare]} { + cd $_gitworktree + } + } else { + cd $current_diff_path + if {$revs eq {--}} { + set s $file_states($current_diff_path) + set old_sha1 {} + set new_sha1 {} + switch -glob -- [lindex $s 0] { + M_ { set old_sha1 [lindex [lindex $s 2] 1] } + _M { set old_sha1 [lindex [lindex $s 3] 1] } + MM { + if {$current_diff_side eq $ui_index} { + set old_sha1 [lindex [lindex $s 2] 1] + set new_sha1 [lindex [lindex $s 3] 1] + } else { + set old_sha1 [lindex [lindex $s 3] 1] + } + } + } + set revs $old_sha1...$new_sha1 + } + # GIT_DIR and GIT_WORK_TREE for the submodule are not the ones + # we've been using for the main repository, so unset them. + # TODO we could make life easier (start up faster?) for gitk + # by setting these to the appropriate values to allow gitk + # to skip the heuristics to find their proper value + unset env(GIT_DIR) + unset env(GIT_WORK_TREE) + } + eval exec $cmd $revs "--" "--" & + + set env(GIT_DIR) $_gitdir + set env(GIT_WORK_TREE) $_gitworktree + cd $pwd + + if {[info exists main_status]} { + set status_operation [$::main_status \ + start \ + [mc "Starting %s... please wait..." "gitk"]] + + after 3500 [list $status_operation stop] + } + } +} + +proc do_git_gui {} { + global current_diff_path + + # -- Always start git gui through whatever we were loaded with. This + # lets us bypass using shell process on Windows systems. + # + set exe [list [_which git]] + if {$exe eq {}} { + error_popup [mc "Couldn't find git gui in PATH"] + } else { + global env + global _gitdir _gitworktree + + # see note in do_gitk about unsetting these vars when + # running tools in a submodule + unset env(GIT_DIR) + unset env(GIT_WORK_TREE) + + set pwd [pwd] + cd $current_diff_path + + eval exec $exe gui & + + set env(GIT_DIR) $_gitdir + set env(GIT_WORK_TREE) $_gitworktree + cd $pwd + + set status_operation [$::main_status \ + start \ + [mc "Starting %s... please wait..." "git-gui"]] + + after 3500 [list $status_operation stop] + } +} + +# Get the system-specific explorer app/command. +proc get_explorer {} { + if {[is_Cygwin] || [is_Windows]} { + set explorer "explorer.exe" + } elseif {[is_MacOSX]} { + set explorer "open" + } else { + # freedesktop.org-conforming system is our best shot + set explorer "xdg-open" + } + return $explorer +} + +proc do_explore {} { + global _gitworktree + set explorer [get_explorer] + eval exec $explorer [list [file nativename $_gitworktree]] & +} + +# Open file relative to the working tree by the default associated app. +proc do_file_open {file} { + global _gitworktree + set explorer [get_explorer] + set full_file_path [file join $_gitworktree $file] + exec $explorer [file nativename $full_file_path] & +} + +set is_quitting 0 +set ret_code 1 + +proc terminate_me {win} { + global ret_code + if {$win ne {.}} return + exit $ret_code +} + +proc do_quit {{rc {1}}} { + global ui_comm is_quitting repo_config commit_type + global GITGUI_BCK_exists GITGUI_BCK_i + global ui_comm_spell + global ret_code use_ttk + + if {$is_quitting} return + set is_quitting 1 + + if {[winfo exists $ui_comm]} { + # -- Stash our current commit buffer. + # + set save [gitdir GITGUI_MSG] + if {$GITGUI_BCK_exists && ![$ui_comm edit modified]} { + file rename -force [gitdir GITGUI_BCK] $save + set GITGUI_BCK_exists 0 + } else { + set msg [string trim [$ui_comm get 0.0 end]] + regsub -all -line {[ \r\t]+$} $msg {} msg + if {(![string match amend* $commit_type] + || [$ui_comm edit modified]) + && $msg ne {}} { + catch { + set fd [open $save w] + fconfigure $fd -encoding utf-8 + puts -nonewline $fd $msg + close $fd + } + } else { + catch {file delete $save} + } + } + + # -- Cancel our spellchecker if its running. + # + if {[info exists ui_comm_spell]} { + $ui_comm_spell stop + } + + # -- Remove our editor backup, its not needed. + # + after cancel $GITGUI_BCK_i + if {$GITGUI_BCK_exists} { + catch {file delete [gitdir GITGUI_BCK]} + } + + # -- Stash our current window geometry into this repository. + # + set cfg_wmstate [wm state .] + if {[catch {set rc_wmstate $repo_config(gui.wmstate)}]} { + set rc_wmstate {} + } + if {$cfg_wmstate ne $rc_wmstate} { + catch {git config gui.wmstate $cfg_wmstate} + } + if {$cfg_wmstate eq {zoomed}} { + # on Windows wm geometry will lie about window + # position (but not size) when window is zoomed + # restore the window before querying wm geometry + wm state . normal + } + set cfg_geometry [list] + lappend cfg_geometry [wm geometry .] + if {$use_ttk} { + lappend cfg_geometry [.vpane sashpos 0] + lappend cfg_geometry [.vpane.files sashpos 0] + } else { + lappend cfg_geometry [lindex [.vpane sash coord 0] 0] + lappend cfg_geometry [lindex [.vpane.files sash coord 0] 1] + } + if {[catch {set rc_geometry $repo_config(gui.geometry)}]} { + set rc_geometry {} + } + if {$cfg_geometry ne $rc_geometry} { + catch {git config gui.geometry $cfg_geometry} + } + } + + set ret_code $rc + + # Briefly enable send again, working around Tk bug + # http://sourceforge.net/tracker/?func=detail&atid=112997&aid=1821174&group_id=12997 + tk appname [appname] + + destroy . +} + +proc do_rescan {} { + rescan ui_ready +} + +proc ui_do_rescan {} { + rescan {force_first_diff ui_ready} +} + +proc do_commit {} { + commit_tree +} + +proc next_diff {{after {}}} { + global next_diff_p next_diff_w next_diff_i + show_diff $next_diff_p $next_diff_w {} {} $after +} + +proc find_anchor_pos {lst name} { + set lid [lsearch -sorted -exact $lst $name] + + if {$lid == -1} { + set lid 0 + foreach lname $lst { + if {$lname >= $name} break + incr lid + } + } + + return $lid +} + +proc find_file_from {flist idx delta path mmask} { + global file_states + + set len [llength $flist] + while {$idx >= 0 && $idx < $len} { + set name [lindex $flist $idx] + + if {$name ne $path && [info exists file_states($name)]} { + set state [lindex $file_states($name) 0] + + if {$mmask eq {} || [regexp $mmask $state]} { + return $idx + } + } + + incr idx $delta + } + + return {} +} + +proc find_next_diff {w path {lno {}} {mmask {}}} { + global next_diff_p next_diff_w next_diff_i + global file_lists ui_index ui_workdir + + set flist $file_lists($w) + if {$lno eq {}} { + set lno [find_anchor_pos $flist $path] + } else { + incr lno -1 + } + + if {$mmask ne {} && ![regexp {(^\^)|(\$$)} $mmask]} { + if {$w eq $ui_index} { + set mmask "^$mmask" + } else { + set mmask "$mmask\$" + } + } + + set idx [find_file_from $flist $lno 1 $path $mmask] + if {$idx eq {}} { + incr lno -1 + set idx [find_file_from $flist $lno -1 $path $mmask] + } + + if {$idx ne {}} { + set next_diff_w $w + set next_diff_p [lindex $flist $idx] + set next_diff_i [expr {$idx+1}] + return 1 + } else { + return 0 + } +} + +proc next_diff_after_action {w path {lno {}} {mmask {}}} { + global current_diff_path + + if {$path ne $current_diff_path} { + return {} + } elseif {[find_next_diff $w $path $lno $mmask]} { + return {next_diff;} + } else { + return {reshow_diff;} + } +} + +proc select_first_diff {after} { + global ui_workdir + + if {[find_next_diff $ui_workdir {} 1 {^_?U}] || + [find_next_diff $ui_workdir {} 1 {[^O]$}]} { + next_diff $after + } else { + uplevel #0 $after + } +} + +proc force_first_diff {after} { + global ui_workdir current_diff_path file_states + + if {[info exists file_states($current_diff_path)]} { + set state [lindex $file_states($current_diff_path) 0] + } else { + set state {OO} + } + + set reselect 0 + if {[string first {U} $state] >= 0} { + # Already a conflict, do nothing + } elseif {[find_next_diff $ui_workdir $current_diff_path {} {^_?U}]} { + set reselect 1 + } elseif {[string index $state 1] ne {O}} { + # Already a diff & no conflicts, do nothing + } elseif {[find_next_diff $ui_workdir $current_diff_path {} {[^O]$}]} { + set reselect 1 + } + + if {$reselect} { + next_diff $after + } else { + uplevel #0 $after + } +} + +proc toggle_or_diff {mode w args} { + global file_states file_lists current_diff_path ui_index ui_workdir + global last_clicked selected_paths file_lists_last_clicked + + if {$mode eq "click"} { + foreach {x y} $args break + set pos [split [$w index @$x,$y] .] + foreach {lno col} $pos break + } else { + if {$mode eq "toggle"} { + if {$w eq $ui_workdir} { + do_add_selection + set last_clicked {} + return + } + if {$w eq $ui_index} { + do_unstage_selection + set last_clicked {} + return + } + } + + if {$last_clicked ne {}} { + set lno [lindex $last_clicked 1] + } else { + if {![info exists file_lists] + || ![info exists file_lists($w)] + || [llength $file_lists($w)] == 0} { + set last_clicked {} + return + } + set lno [expr {int([lindex [$w tag ranges in_diff] 0])}] + } + if {$mode eq "toggle"} { + set col 0; set y 2 + } else { + incr lno [expr {$mode eq "up" ? -1 : 1}] + set col 1 + } + } + + if {![info exists file_lists] + || ![info exists file_lists($w)] + || [llength $file_lists($w)] < $lno - 1} { + set path {} + } else { + set path [lindex $file_lists($w) [expr {$lno - 1}]] + } + if {$path eq {}} { + set last_clicked {} + return + } + + set last_clicked [list $w $lno] + focus $w + array unset selected_paths + $ui_index tag remove in_sel 0.0 end + $ui_workdir tag remove in_sel 0.0 end + + set file_lists_last_clicked($w) $path + + # Determine the state of the file + if {[info exists file_states($path)]} { + set state [lindex $file_states($path) 0] + } else { + set state {__} + } + + # Restage the file, or simply show the diff + if {$col == 0 && $y > 1} { + # Conflicts need special handling + if {[string first {U} $state] >= 0} { + # $w must always be $ui_workdir, but... + if {$w ne $ui_workdir} { set lno {} } + merge_stage_workdir $path $lno + return + } + + if {[string index $state 1] eq {O}} { + set mmask {} + } else { + set mmask {[^O]} + } + + set after [next_diff_after_action $w $path $lno $mmask] + + if {$w eq $ui_index} { + update_indexinfo \ + "Unstaging [short_path $path] from commit" \ + [list $path] \ + [concat $after [list ui_ready]] + } elseif {$w eq $ui_workdir} { + update_index \ + "Adding [short_path $path]" \ + [list $path] \ + [concat $after [list ui_ready]] + } + } else { + set selected_paths($path) 1 + show_diff $path $w $lno + } +} + +proc add_one_to_selection {w x y} { + global file_lists last_clicked selected_paths + + set lno [lindex [split [$w index @$x,$y] .] 0] + set path [lindex $file_lists($w) [expr {$lno - 1}]] + if {$path eq {}} { + set last_clicked {} + return + } + + if {$last_clicked ne {} + && [lindex $last_clicked 0] ne $w} { + array unset selected_paths + [lindex $last_clicked 0] tag remove in_sel 0.0 end + } + + set last_clicked [list $w $lno] + if {[catch {set in_sel $selected_paths($path)}]} { + set in_sel 0 + } + if {$in_sel} { + unset selected_paths($path) + $w tag remove in_sel $lno.0 [expr {$lno + 1}].0 + } else { + set selected_paths($path) 1 + $w tag add in_sel $lno.0 [expr {$lno + 1}].0 + } +} + +proc add_range_to_selection {w x y} { + global file_lists last_clicked selected_paths + + if {[lindex $last_clicked 0] ne $w} { + toggle_or_diff click $w $x $y + return + } + + set lno [lindex [split [$w index @$x,$y] .] 0] + set lc [lindex $last_clicked 1] + if {$lc < $lno} { + set begin $lc + set end $lno + } else { + set begin $lno + set end $lc + } + + foreach path [lrange $file_lists($w) \ + [expr {$begin - 1}] \ + [expr {$end - 1}]] { + set selected_paths($path) 1 + } + $w tag add in_sel $begin.0 [expr {$end + 1}].0 +} + +proc show_more_context {} { + global repo_config + if {$repo_config(gui.diffcontext) < 99} { + incr repo_config(gui.diffcontext) + reshow_diff + } +} + +proc show_less_context {} { + global repo_config + if {$repo_config(gui.diffcontext) > 1} { + incr repo_config(gui.diffcontext) -1 + reshow_diff + } +} + +proc focus_widget {widget} { + global file_lists last_clicked selected_paths + global file_lists_last_clicked + + if {[llength $file_lists($widget)] > 0} { + set path $file_lists_last_clicked($widget) + set index [lsearch -sorted -exact $file_lists($widget) $path] + if {$index < 0} { + set index 0 + set path [lindex $file_lists($widget) $index] + } + + focus $widget + set last_clicked [list $widget [expr $index + 1]] + array unset selected_paths + set selected_paths($path) 1 + show_diff $path $widget + } +} + +proc toggle_commit_type {} { + global commit_type_is_amend + set commit_type_is_amend [expr !$commit_type_is_amend] + do_select_commit_type +} + +###################################################################### +## +## ui construction + +set ui_comm {} + +# -- Menu Bar +# +menu .mbar -tearoff 0 +if {[is_MacOSX]} { + # -- Apple Menu (Mac OS X only) + # + .mbar add cascade -label Apple -menu .mbar.apple + menu .mbar.apple +} +.mbar add cascade -label [mc Repository] -menu .mbar.repository +.mbar add cascade -label [mc Edit] -menu .mbar.edit +if {[is_enabled branch]} { + .mbar add cascade -label [mc Branch] -menu .mbar.branch +} +if {[is_enabled multicommit] || [is_enabled singlecommit]} { + .mbar add cascade -label [mc Commit@@noun] -menu .mbar.commit +} +if {[is_enabled transport]} { + .mbar add cascade -label [mc Merge] -menu .mbar.merge + .mbar add cascade -label [mc Remote] -menu .mbar.remote +} +if {[is_enabled multicommit] || [is_enabled singlecommit]} { + .mbar add cascade -label [mc Tools] -menu .mbar.tools +} + +# -- Repository Menu +# +menu .mbar.repository + +if {![is_bare]} { + .mbar.repository add command \ + -label [mc "Explore Working Copy"] \ + -command {do_explore} +} + +if {[is_Windows]} { + # Use /git-bash.exe if available + set normalized [file normalize $::argv0] + regsub "/mingw../libexec/git-core/git-gui$" \ + $normalized "/git-bash.exe" cmdLine + if {$cmdLine != $normalized && [file exists $cmdLine]} { + set cmdLine [list "Git Bash" $cmdLine &] + } else { + set cmdLine [list "Git Bash" bash --login -l &] + } + .mbar.repository add command \ + -label [mc "Git Bash"] \ + -command {eval exec [auto_execok start] $cmdLine} +} + +if {[is_Windows] || ![is_bare]} { + .mbar.repository add separator +} + +.mbar.repository add command \ + -label [mc "Browse Current Branch's Files"] \ + -command {browser::new $current_branch} +set ui_browse_current [.mbar.repository index last] +.mbar.repository add command \ + -label [mc "Browse Branch Files..."] \ + -command browser_open::dialog +.mbar.repository add separator + +.mbar.repository add command \ + -label [mc "Visualize Current Branch's History"] \ + -command {do_gitk $current_branch} +set ui_visualize_current [.mbar.repository index last] +.mbar.repository add command \ + -label [mc "Visualize All Branch History"] \ + -command {do_gitk --all} +.mbar.repository add separator + +proc current_branch_write {args} { + global current_branch + .mbar.repository entryconf $::ui_browse_current \ + -label [mc "Browse %s's Files" $current_branch] + .mbar.repository entryconf $::ui_visualize_current \ + -label [mc "Visualize %s's History" $current_branch] +} +trace add variable current_branch write current_branch_write + +if {[is_enabled multicommit]} { + .mbar.repository add command -label [mc "Database Statistics"] \ + -command do_stats + + .mbar.repository add command -label [mc "Compress Database"] \ + -command do_gc + + .mbar.repository add command -label [mc "Verify Database"] \ + -command do_fsck_objects + + .mbar.repository add separator + + if {[is_Cygwin]} { + .mbar.repository add command \ + -label [mc "Create Desktop Icon"] \ + -command do_cygwin_shortcut + } elseif {[is_Windows]} { + .mbar.repository add command \ + -label [mc "Create Desktop Icon"] \ + -command do_windows_shortcut + } elseif {[is_MacOSX]} { + .mbar.repository add command \ + -label [mc "Create Desktop Icon"] \ + -command do_macosx_app + } +} + +if {[is_MacOSX]} { + proc ::tk::mac::Quit {args} { do_quit } +} else { + .mbar.repository add command -label [mc Quit] \ + -command do_quit \ + -accelerator $M1T-Q +} + +# -- Edit Menu +# +menu .mbar.edit +.mbar.edit add command -label [mc Undo] \ + -command {catch {[focus] edit undo}} \ + -accelerator $M1T-Z +.mbar.edit add command -label [mc Redo] \ + -command {catch {[focus] edit redo}} \ + -accelerator $M1T-Y +.mbar.edit add separator +.mbar.edit add command -label [mc Cut] \ + -command {catch {tk_textCut [focus]}} \ + -accelerator $M1T-X +.mbar.edit add command -label [mc Copy] \ + -command {catch {tk_textCopy [focus]}} \ + -accelerator $M1T-C +.mbar.edit add command -label [mc Paste] \ + -command {catch {tk_textPaste [focus]; [focus] see insert}} \ + -accelerator $M1T-V +.mbar.edit add command -label [mc Delete] \ + -command {catch {[focus] delete sel.first sel.last}} \ + -accelerator Del +.mbar.edit add separator +.mbar.edit add command -label [mc "Select All"] \ + -command {catch {[focus] tag add sel 0.0 end}} \ + -accelerator $M1T-A + +# -- Branch Menu +# +if {[is_enabled branch]} { + menu .mbar.branch + + .mbar.branch add command -label [mc "Create..."] \ + -command branch_create::dialog \ + -accelerator $M1T-N + lappend disable_on_lock [list .mbar.branch entryconf \ + [.mbar.branch index last] -state] + + .mbar.branch add command -label [mc "Checkout..."] \ + -command branch_checkout::dialog \ + -accelerator $M1T-O + lappend disable_on_lock [list .mbar.branch entryconf \ + [.mbar.branch index last] -state] + + .mbar.branch add command -label [mc "Rename..."] \ + -command branch_rename::dialog + lappend disable_on_lock [list .mbar.branch entryconf \ + [.mbar.branch index last] -state] + + .mbar.branch add command -label [mc "Delete..."] \ + -command branch_delete::dialog + lappend disable_on_lock [list .mbar.branch entryconf \ + [.mbar.branch index last] -state] + + .mbar.branch add command -label [mc "Reset..."] \ + -command merge::reset_hard + lappend disable_on_lock [list .mbar.branch entryconf \ + [.mbar.branch index last] -state] +} + +# -- Commit Menu +# +proc commit_btn_caption {} { + if {[is_enabled nocommit]} { + return [mc "Done"] + } else { + return [mc Commit@@verb] + } +} + +if {[is_enabled multicommit] || [is_enabled singlecommit]} { + menu .mbar.commit + + if {![is_enabled nocommit]} { + .mbar.commit add checkbutton \ + -label [mc "Amend Last Commit"] \ + -accelerator $M1T-E \ + -variable commit_type_is_amend \ + -command do_select_commit_type + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] + + .mbar.commit add separator + } + + .mbar.commit add command -label [mc Rescan] \ + -command ui_do_rescan \ + -accelerator F5 + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] + + .mbar.commit add command -label [mc "Stage To Commit"] \ + -command do_add_selection \ + -accelerator $M1T-T + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] + + .mbar.commit add command -label [mc "Stage Changed Files To Commit"] \ + -command do_add_all \ + -accelerator $M1T-I + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] + + .mbar.commit add command -label [mc "Unstage From Commit"] \ + -command do_unstage_selection \ + -accelerator $M1T-U + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] + + .mbar.commit add command -label [mc "Revert Changes"] \ + -command do_revert_selection \ + -accelerator $M1T-J + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] + + .mbar.commit add separator + + .mbar.commit add command -label [mc "Show Less Context"] \ + -command show_less_context \ + -accelerator $M1T-\- + + .mbar.commit add command -label [mc "Show More Context"] \ + -command show_more_context \ + -accelerator $M1T-= + + .mbar.commit add separator + + if {![is_enabled nocommitmsg]} { + .mbar.commit add command -label [mc "Sign Off"] \ + -command do_signoff \ + -accelerator $M1T-S + } + + .mbar.commit add command -label [commit_btn_caption] \ + -command do_commit \ + -accelerator $M1T-Return + lappend disable_on_lock \ + [list .mbar.commit entryconf [.mbar.commit index last] -state] +} + +# -- Merge Menu +# +if {[is_enabled branch]} { + menu .mbar.merge + .mbar.merge add command -label [mc "Local Merge..."] \ + -command merge::dialog \ + -accelerator $M1T-M + lappend disable_on_lock \ + [list .mbar.merge entryconf [.mbar.merge index last] -state] + .mbar.merge add command -label [mc "Abort Merge..."] \ + -command merge::reset_hard + lappend disable_on_lock \ + [list .mbar.merge entryconf [.mbar.merge index last] -state] +} + +# -- Transport Menu +# +if {[is_enabled transport]} { + menu .mbar.remote + + .mbar.remote add command \ + -label [mc "Add..."] \ + -command remote_add::dialog \ + -accelerator $M1T-A + .mbar.remote add command \ + -label [mc "Push..."] \ + -command do_push_anywhere \ + -accelerator $M1T-P + .mbar.remote add command \ + -label [mc "Delete Branch..."] \ + -command remote_branch_delete::dialog +} + +if {[is_MacOSX]} { + proc ::tk::mac::ShowPreferences {} {do_options} +} else { + # -- Edit Menu + # + .mbar.edit add separator + .mbar.edit add command -label [mc "Options..."] \ + -command do_options +} + +# -- Tools Menu +# +if {[is_enabled multicommit] || [is_enabled singlecommit]} { + set tools_menubar .mbar.tools + menu $tools_menubar + $tools_menubar add separator + $tools_menubar add command -label [mc "Add..."] -command tools_add::dialog + $tools_menubar add command -label [mc "Remove..."] -command tools_remove::dialog + set tools_tailcnt 3 + if {[array names repo_config guitool.*.cmd] ne {}} { + tools_populate_all + } +} + +# -- Help Menu +# +.mbar add cascade -label [mc Help] -menu .mbar.help +menu .mbar.help + +if {[is_MacOSX]} { + .mbar.apple add command -label [mc "About %s" [appname]] \ + -command do_about + .mbar.apple add separator +} else { + .mbar.help add command -label [mc "About %s" [appname]] \ + -command do_about +} +. configure -menu .mbar + +set doc_path [githtmldir] +if {$doc_path ne {}} { + set doc_path [file join $doc_path index.html] + + if {[is_Cygwin]} { + set doc_path [exec cygpath --mixed $doc_path] + } +} + +if {[file isfile $doc_path]} { + set doc_url "file:$doc_path" +} else { + set doc_url {http://www.kernel.org/pub/software/scm/git/docs/} +} + +proc start_browser {url} { + git "web--browse" $url +} + +.mbar.help add command -label [mc "Online Documentation"] \ + -command [list start_browser $doc_url] + +.mbar.help add command -label [mc "Show SSH Key"] \ + -command do_ssh_key + +unset doc_path doc_url + +# -- Standard bindings +# +wm protocol . WM_DELETE_WINDOW do_quit +bind all <$M1B-Key-q> do_quit +bind all <$M1B-Key-Q> do_quit + +set m1b_w_script { + set toplvl_win [winfo toplevel %W] + + # If we are destroying the main window, we should call do_quit to take + # care of cleanup before exiting the program. + if {$toplvl_win eq "."} { + do_quit + } else { + destroy $toplvl_win + } +} + +bind all <$M1B-Key-w> $m1b_w_script +bind all <$M1B-Key-W> $m1b_w_script + +unset m1b_w_script + +set subcommand_args {} +proc usage {} { + set s "[mc usage:] $::argv0 $::subcommand $::subcommand_args" + if {[tk windowingsystem] eq "win32"} { + wm withdraw . + tk_messageBox -icon info -message $s \ + -title [mc "Usage"] + } else { + puts stderr $s + } + exit 1 +} + +proc normalize_relpath {path} { + set elements {} + foreach item [file split $path] { + if {$item eq {.}} continue + if {$item eq {..} && [llength $elements] > 0 + && [lindex $elements end] ne {..}} { + set elements [lrange $elements 0 end-1] + continue + } + lappend elements $item + } + return [eval file join $elements] +} + +# -- Not a normal commit type invocation? Do that instead! +# +switch -- $subcommand { +browser - +blame { + if {$subcommand eq "blame"} { + set subcommand_args {[--line=<num>] rev? path} + } else { + set subcommand_args {rev? path} + } + if {$argv eq {}} usage + set head {} + set path {} + set jump_spec {} + set is_path 0 + foreach a $argv { + set p [file join $_prefix $a] + + if {$is_path || [file exists $p]} { + if {$path ne {}} usage + set path [normalize_relpath $p] + break + } elseif {$a eq {--}} { + if {$path ne {}} { + if {$head ne {}} usage + set head $path + set path {} + } + set is_path 1 + } elseif {[regexp {^--line=(\d+)$} $a a lnum]} { + if {$jump_spec ne {} || $head ne {}} usage + set jump_spec [list $lnum] + } elseif {$head eq {}} { + if {$head ne {}} usage + set head $a + set is_path 1 + } else { + usage + } + } + unset is_path + + if {$head ne {} && $path eq {}} { + if {[string index $head 0] eq {/}} { + set path [normalize_relpath $head] + set head {} + } else { + set path [normalize_relpath $_prefix$head] + set head {} + } + } + + if {$head eq {}} { + load_current_branch + } else { + if {[regexp {^[0-9a-f]{1,39}$} $head]} { + if {[catch { + set head [git rev-parse --verify $head] + } err]} { + if {[tk windowingsystem] eq "win32"} { + tk_messageBox -icon error -title [mc Error] -message $err + } else { + puts stderr $err + } + exit 1 + } + } + set current_branch $head + } + + wm deiconify . + switch -- $subcommand { + browser { + if {$jump_spec ne {}} usage + if {$head eq {}} { + if {$path ne {} && [file isdirectory $path]} { + set head $current_branch + } else { + set head $path + set path {} + } + } + browser::new $head $path + } + blame { + if {$head eq {} && ![file exists $path]} { + catch {wm withdraw .} + tk_messageBox \ + -icon error \ + -type ok \ + -title [mc "git-gui: fatal error"] \ + -message [mc "fatal: cannot stat path %s: No such file or directory" $path] + exit 1 + } + blame::new $head $path $jump_spec + } + } + return +} +citool - +gui { + if {[llength $argv] != 0} { + usage + } + # fall through to setup UI for commits +} +default { + set err "[mc usage:] $argv0 \[{blame|browser|citool}\]" + if {[tk windowingsystem] eq "win32"} { + wm withdraw . + tk_messageBox -icon error -message $err \ + -title [mc "Usage"] + } else { + puts stderr $err + } + exit 1 +} +} + +# -- Branch Control +# +${NS}::frame .branch +if {!$use_ttk} {.branch configure -borderwidth 1 -relief sunken} +${NS}::label .branch.l1 \ + -text [mc "Current Branch:"] \ + -anchor w \ + -justify left +${NS}::label .branch.cb \ + -textvariable current_branch \ + -anchor w \ + -justify left +pack .branch.l1 -side left +pack .branch.cb -side left -fill x +pack .branch -side top -fill x + +# -- Main Window Layout +# +${NS}::panedwindow .vpane -orient horizontal +${NS}::panedwindow .vpane.files -orient vertical +if {$use_ttk} { + .vpane add .vpane.files +} else { + .vpane add .vpane.files -sticky nsew -height 100 -width 200 +} +pack .vpane -anchor n -side top -fill both -expand 1 + +# -- Working Directory File List + +textframe .vpane.files.workdir -height 100 -width 200 +tlabel .vpane.files.workdir.title -text [mc "Unstaged Changes"] \ + -background lightsalmon -foreground black +ttext $ui_workdir -background white -foreground black \ + -borderwidth 0 \ + -width 20 -height 10 \ + -wrap none \ + -takefocus 1 -highlightthickness 1\ + -cursor $cursor_ptr \ + -xscrollcommand {.vpane.files.workdir.sx set} \ + -yscrollcommand {.vpane.files.workdir.sy set} \ + -state disabled +${NS}::scrollbar .vpane.files.workdir.sx -orient h -command [list $ui_workdir xview] +${NS}::scrollbar .vpane.files.workdir.sy -orient v -command [list $ui_workdir yview] +pack .vpane.files.workdir.title -side top -fill x +pack .vpane.files.workdir.sx -side bottom -fill x +pack .vpane.files.workdir.sy -side right -fill y +pack $ui_workdir -side left -fill both -expand 1 + +# -- Index File List +# +textframe .vpane.files.index -height 100 -width 200 +tlabel .vpane.files.index.title \ + -text [mc "Staged Changes (Will Commit)"] \ + -background lightgreen -foreground black +ttext $ui_index -background white -foreground black \ + -borderwidth 0 \ + -width 20 -height 10 \ + -wrap none \ + -takefocus 1 -highlightthickness 1\ + -cursor $cursor_ptr \ + -xscrollcommand {.vpane.files.index.sx set} \ + -yscrollcommand {.vpane.files.index.sy set} \ + -state disabled +${NS}::scrollbar .vpane.files.index.sx -orient h -command [list $ui_index xview] +${NS}::scrollbar .vpane.files.index.sy -orient v -command [list $ui_index yview] +pack .vpane.files.index.title -side top -fill x +pack .vpane.files.index.sx -side bottom -fill x +pack .vpane.files.index.sy -side right -fill y +pack $ui_index -side left -fill both -expand 1 + +# -- Insert the workdir and index into the panes +# +.vpane.files add .vpane.files.workdir +.vpane.files add .vpane.files.index +if {!$use_ttk} { + .vpane.files paneconfigure .vpane.files.workdir -sticky news + .vpane.files paneconfigure .vpane.files.index -sticky news +} + +foreach i [list $ui_index $ui_workdir] { + rmsel_tag $i + $i tag conf in_diff -background [$i tag cget in_sel -background] +} +unset i + +# -- Diff and Commit Area +# +if {$have_tk85} { + ${NS}::panedwindow .vpane.lower -orient vertical + ${NS}::frame .vpane.lower.commarea + ${NS}::frame .vpane.lower.diff -relief sunken -borderwidth 1 -height 500 + .vpane.lower add .vpane.lower.diff + .vpane.lower add .vpane.lower.commarea + .vpane add .vpane.lower + if {$use_ttk} { + .vpane.lower pane .vpane.lower.diff -weight 1 + .vpane.lower pane .vpane.lower.commarea -weight 0 + } else { + .vpane.lower paneconfigure .vpane.lower.diff -stretch always + .vpane.lower paneconfigure .vpane.lower.commarea -stretch never + } +} else { + frame .vpane.lower -height 300 -width 400 + frame .vpane.lower.commarea + frame .vpane.lower.diff -relief sunken -borderwidth 1 + pack .vpane.lower.diff -fill both -expand 1 + pack .vpane.lower.commarea -side bottom -fill x + .vpane add .vpane.lower + .vpane paneconfigure .vpane.lower -sticky nsew +} + +# -- Commit Area Buttons +# +${NS}::frame .vpane.lower.commarea.buttons +${NS}::label .vpane.lower.commarea.buttons.l -text {} \ + -anchor w \ + -justify left +pack .vpane.lower.commarea.buttons.l -side top -fill x +pack .vpane.lower.commarea.buttons -side left -fill y + +${NS}::button .vpane.lower.commarea.buttons.rescan -text [mc Rescan] \ + -command ui_do_rescan +pack .vpane.lower.commarea.buttons.rescan -side top -fill x +lappend disable_on_lock \ + {.vpane.lower.commarea.buttons.rescan conf -state} + +${NS}::button .vpane.lower.commarea.buttons.incall -text [mc "Stage Changed"] \ + -command do_add_all +pack .vpane.lower.commarea.buttons.incall -side top -fill x +lappend disable_on_lock \ + {.vpane.lower.commarea.buttons.incall conf -state} + +if {![is_enabled nocommitmsg]} { + ${NS}::button .vpane.lower.commarea.buttons.signoff -text [mc "Sign Off"] \ + -command do_signoff + pack .vpane.lower.commarea.buttons.signoff -side top -fill x +} + +${NS}::button .vpane.lower.commarea.buttons.commit -text [commit_btn_caption] \ + -command do_commit +pack .vpane.lower.commarea.buttons.commit -side top -fill x +lappend disable_on_lock \ + {.vpane.lower.commarea.buttons.commit conf -state} + +if {![is_enabled nocommit]} { + ${NS}::button .vpane.lower.commarea.buttons.push -text [mc Push] \ + -command do_push_anywhere + pack .vpane.lower.commarea.buttons.push -side top -fill x +} + +# -- Commit Message Buffer +# +${NS}::frame .vpane.lower.commarea.buffer +${NS}::frame .vpane.lower.commarea.buffer.header +set ui_comm .vpane.lower.commarea.buffer.frame.t +set ui_coml .vpane.lower.commarea.buffer.header.l + +if {![is_enabled nocommit]} { + ${NS}::checkbutton .vpane.lower.commarea.buffer.header.amend \ + -text [mc "Amend Last Commit"] \ + -variable commit_type_is_amend \ + -command do_select_commit_type + lappend disable_on_lock \ + [list .vpane.lower.commarea.buffer.header.amend conf -state] +} + +${NS}::label $ui_coml \ + -anchor w \ + -justify left +proc trace_commit_type {varname args} { + global ui_coml commit_type + switch -glob -- $commit_type { + initial {set txt [mc "Initial Commit Message:"]} + amend {set txt [mc "Amended Commit Message:"]} + amend-initial {set txt [mc "Amended Initial Commit Message:"]} + amend-merge {set txt [mc "Amended Merge Commit Message:"]} + merge {set txt [mc "Merge Commit Message:"]} + * {set txt [mc "Commit Message:"]} + } + $ui_coml conf -text $txt +} +trace add variable commit_type write trace_commit_type +pack $ui_coml -side left -fill x + +if {![is_enabled nocommit]} { + pack .vpane.lower.commarea.buffer.header.amend -side right +} + +textframe .vpane.lower.commarea.buffer.frame +ttext $ui_comm -background white -foreground black \ + -borderwidth 1 \ + -undo true \ + -maxundo 20 \ + -autoseparators true \ + -takefocus 1 \ + -highlightthickness 1 \ + -relief sunken \ + -width $repo_config(gui.commitmsgwidth) -height 9 -wrap none \ + -font font_diff \ + -xscrollcommand {.vpane.lower.commarea.buffer.frame.sbx set} \ + -yscrollcommand {.vpane.lower.commarea.buffer.frame.sby set} +${NS}::scrollbar .vpane.lower.commarea.buffer.frame.sbx \ + -orient horizontal \ + -command [list $ui_comm xview] +${NS}::scrollbar .vpane.lower.commarea.buffer.frame.sby \ + -orient vertical \ + -command [list $ui_comm yview] + +pack .vpane.lower.commarea.buffer.frame.sbx -side bottom -fill x +pack .vpane.lower.commarea.buffer.frame.sby -side right -fill y +pack $ui_comm -side left -fill y +pack .vpane.lower.commarea.buffer.header -side top -fill x +pack .vpane.lower.commarea.buffer.frame -side left -fill y +pack .vpane.lower.commarea.buffer -side left -fill y + +# -- Commit Message Buffer Context Menu +# +set ctxm .vpane.lower.commarea.buffer.ctxm +menu $ctxm -tearoff 0 +$ctxm add command \ + -label [mc Cut] \ + -command {tk_textCut $ui_comm} +$ctxm add command \ + -label [mc Copy] \ + -command {tk_textCopy $ui_comm} +$ctxm add command \ + -label [mc Paste] \ + -command {tk_textPaste $ui_comm} +$ctxm add command \ + -label [mc Delete] \ + -command {catch {$ui_comm delete sel.first sel.last}} +$ctxm add separator +$ctxm add command \ + -label [mc "Select All"] \ + -command {focus $ui_comm;$ui_comm tag add sel 0.0 end} +$ctxm add command \ + -label [mc "Copy All"] \ + -command { + $ui_comm tag add sel 0.0 end + tk_textCopy $ui_comm + $ui_comm tag remove sel 0.0 end + } +$ctxm add separator +$ctxm add command \ + -label [mc "Sign Off"] \ + -command do_signoff +set ui_comm_ctxm $ctxm + +# -- Diff Header +# +proc trace_current_diff_path {varname args} { + global current_diff_path diff_actions file_states + if {$current_diff_path eq {}} { + set s {} + set f {} + set p {} + set o disabled + } else { + set p $current_diff_path + set s [mapdesc [lindex $file_states($p) 0] $p] + set f [mc "File:"] + set p [escape_path $p] + set o normal + } + + .vpane.lower.diff.header.status configure -text $s + .vpane.lower.diff.header.file configure -text $f + .vpane.lower.diff.header.path configure -text $p + foreach w $diff_actions { + uplevel #0 $w $o + } +} +trace add variable current_diff_path write trace_current_diff_path + +gold_frame .vpane.lower.diff.header +tlabel .vpane.lower.diff.header.status \ + -background gold \ + -foreground black \ + -width $max_status_desc \ + -anchor w \ + -justify left +tlabel .vpane.lower.diff.header.file \ + -background gold \ + -foreground black \ + -anchor w \ + -justify left +tlabel .vpane.lower.diff.header.path \ + -background gold \ + -foreground blue \ + -anchor w \ + -justify left \ + -font [eval font create [font configure font_ui] -underline 1] \ + -cursor hand2 +pack .vpane.lower.diff.header.status -side left +pack .vpane.lower.diff.header.file -side left +pack .vpane.lower.diff.header.path -fill x +set ctxm .vpane.lower.diff.header.ctxm +menu $ctxm -tearoff 0 +$ctxm add command \ + -label [mc Copy] \ + -command { + clipboard clear + clipboard append \ + -format STRING \ + -type STRING \ + -- $current_diff_path + } +$ctxm add command \ + -label [mc Open] \ + -command {do_file_open $current_diff_path} +lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] +bind_button3 .vpane.lower.diff.header.path "tk_popup $ctxm %X %Y" +bind .vpane.lower.diff.header.path <Button-1> {do_file_open $current_diff_path} + +# -- Diff Body +# +textframe .vpane.lower.diff.body +set ui_diff .vpane.lower.diff.body.t +ttext $ui_diff -background white -foreground black \ + -borderwidth 0 \ + -width 80 -height 5 -wrap none \ + -font font_diff \ + -takefocus 1 -highlightthickness 1 \ + -xscrollcommand {.vpane.lower.diff.body.sbx set} \ + -yscrollcommand {.vpane.lower.diff.body.sby set} \ + -state disabled +catch {$ui_diff configure -tabstyle wordprocessor} +${NS}::scrollbar .vpane.lower.diff.body.sbx -orient horizontal \ + -command [list $ui_diff xview] +${NS}::scrollbar .vpane.lower.diff.body.sby -orient vertical \ + -command [list $ui_diff yview] +pack .vpane.lower.diff.body.sbx -side bottom -fill x +pack .vpane.lower.diff.body.sby -side right -fill y +pack $ui_diff -side left -fill both -expand 1 +pack .vpane.lower.diff.header -side top -fill x +pack .vpane.lower.diff.body -side bottom -fill both -expand 1 + +foreach {n c} {0 black 1 red4 2 green4 3 yellow4 4 blue4 5 magenta4 6 cyan4 7 grey60} { + $ui_diff tag configure clr4$n -background $c + $ui_diff tag configure clri4$n -foreground $c + $ui_diff tag configure clr3$n -foreground $c + $ui_diff tag configure clri3$n -background $c +} +$ui_diff tag configure clr1 -font font_diffbold +$ui_diff tag configure clr4 -underline 1 + +$ui_diff tag conf d_info -foreground blue -font font_diffbold + +$ui_diff tag conf d_cr -elide true +$ui_diff tag conf d_@ -font font_diffbold +$ui_diff tag conf d_+ -foreground {#00a000} +$ui_diff tag conf d_- -foreground red + +$ui_diff tag conf d_++ -foreground {#00a000} +$ui_diff tag conf d_-- -foreground red +$ui_diff tag conf d_+s \ + -foreground {#00a000} \ + -background {#e2effa} +$ui_diff tag conf d_-s \ + -foreground red \ + -background {#e2effa} +$ui_diff tag conf d_s+ \ + -foreground {#00a000} \ + -background ivory1 +$ui_diff tag conf d_s- \ + -foreground red \ + -background ivory1 + +$ui_diff tag conf d< \ + -foreground orange \ + -font font_diffbold +$ui_diff tag conf d| \ + -foreground orange \ + -font font_diffbold +$ui_diff tag conf d= \ + -foreground orange \ + -font font_diffbold +$ui_diff tag conf d> \ + -foreground orange \ + -font font_diffbold + +$ui_diff tag raise sel + +# -- Diff Body Context Menu +# + +proc create_common_diff_popup {ctxm} { + $ctxm add command \ + -label [mc Refresh] \ + -command reshow_diff + lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] + $ctxm add command \ + -label [mc Copy] \ + -command {tk_textCopy $ui_diff} + lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] + $ctxm add command \ + -label [mc "Select All"] \ + -command {focus $ui_diff;$ui_diff tag add sel 0.0 end} + lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] + $ctxm add command \ + -label [mc "Copy All"] \ + -command { + $ui_diff tag add sel 0.0 end + tk_textCopy $ui_diff + $ui_diff tag remove sel 0.0 end + } + lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] + $ctxm add separator + $ctxm add command \ + -label [mc "Decrease Font Size"] \ + -command {incr_font_size font_diff -1} + lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] + $ctxm add command \ + -label [mc "Increase Font Size"] \ + -command {incr_font_size font_diff 1} + lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] + $ctxm add separator + set emenu $ctxm.enc + menu $emenu + build_encoding_menu $emenu [list force_diff_encoding] + $ctxm add cascade \ + -label [mc "Encoding"] \ + -menu $emenu + lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] + $ctxm add separator + $ctxm add command -label [mc "Options..."] \ + -command do_options +} + +set ctxm .vpane.lower.diff.body.ctxm +menu $ctxm -tearoff 0 +$ctxm add command \ + -label [mc "Apply/Reverse Hunk"] \ + -command {apply_or_revert_hunk $cursorX $cursorY 0} +set ui_diff_applyhunk [$ctxm index last] +lappend diff_actions [list $ctxm entryconf $ui_diff_applyhunk -state] +$ctxm add command \ + -label [mc "Apply/Reverse Line"] \ + -command {apply_or_revert_range_or_line $cursorX $cursorY 0; do_rescan} +set ui_diff_applyline [$ctxm index last] +lappend diff_actions [list $ctxm entryconf $ui_diff_applyline -state] +$ctxm add separator +$ctxm add command \ + -label [mc "Revert Hunk"] \ + -command {apply_or_revert_hunk $cursorX $cursorY 1} +set ui_diff_reverthunk [$ctxm index last] +lappend diff_actions [list $ctxm entryconf $ui_diff_reverthunk -state] +$ctxm add command \ + -label [mc "Revert Line"] \ + -command {apply_or_revert_range_or_line $cursorX $cursorY 1; do_rescan} +set ui_diff_revertline [$ctxm index last] +lappend diff_actions [list $ctxm entryconf $ui_diff_revertline -state] +$ctxm add command \ + -label [mc "Undo Last Revert"] \ + -command {undo_last_revert; do_rescan} +set ui_diff_undorevert [$ctxm index last] +lappend diff_actions [list $ctxm entryconf $ui_diff_undorevert -state] +$ctxm add separator +$ctxm add command \ + -label [mc "Show Less Context"] \ + -command show_less_context +lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] +$ctxm add command \ + -label [mc "Show More Context"] \ + -command show_more_context +lappend diff_actions [list $ctxm entryconf [$ctxm index last] -state] +$ctxm add separator +create_common_diff_popup $ctxm + +set ctxmmg .vpane.lower.diff.body.ctxmmg +menu $ctxmmg -tearoff 0 +$ctxmmg add command \ + -label [mc "Run Merge Tool"] \ + -command {merge_resolve_tool} +lappend diff_actions [list $ctxmmg entryconf [$ctxmmg index last] -state] +$ctxmmg add separator +$ctxmmg add command \ + -label [mc "Use Remote Version"] \ + -command {merge_resolve_one 3} +lappend diff_actions [list $ctxmmg entryconf [$ctxmmg index last] -state] +$ctxmmg add command \ + -label [mc "Use Local Version"] \ + -command {merge_resolve_one 2} +lappend diff_actions [list $ctxmmg entryconf [$ctxmmg index last] -state] +$ctxmmg add command \ + -label [mc "Revert To Base"] \ + -command {merge_resolve_one 1} +lappend diff_actions [list $ctxmmg entryconf [$ctxmmg index last] -state] +$ctxmmg add separator +$ctxmmg add command \ + -label [mc "Show Less Context"] \ + -command show_less_context +lappend diff_actions [list $ctxmmg entryconf [$ctxmmg index last] -state] +$ctxmmg add command \ + -label [mc "Show More Context"] \ + -command show_more_context +lappend diff_actions [list $ctxmmg entryconf [$ctxmmg index last] -state] +$ctxmmg add separator +create_common_diff_popup $ctxmmg + +set ctxmsm .vpane.lower.diff.body.ctxmsm +menu $ctxmsm -tearoff 0 +$ctxmsm add command \ + -label [mc "Visualize These Changes In The Submodule"] \ + -command {do_gitk -- true} +lappend diff_actions [list $ctxmsm entryconf [$ctxmsm index last] -state] +$ctxmsm add command \ + -label [mc "Visualize Current Branch History In The Submodule"] \ + -command {do_gitk {} true} +lappend diff_actions [list $ctxmsm entryconf [$ctxmsm index last] -state] +$ctxmsm add command \ + -label [mc "Visualize All Branch History In The Submodule"] \ + -command {do_gitk --all true} +lappend diff_actions [list $ctxmsm entryconf [$ctxmsm index last] -state] +$ctxmsm add separator +$ctxmsm add command \ + -label [mc "Start git gui In The Submodule"] \ + -command {do_git_gui} +lappend diff_actions [list $ctxmsm entryconf [$ctxmsm index last] -state] +$ctxmsm add separator +create_common_diff_popup $ctxmsm + +proc has_textconv {path} { + if {[is_config_false gui.textconv]} { + return 0 + } + set filter [gitattr $path diff set] + set textconv [get_config [join [list diff $filter textconv] .]] + if {$filter ne {set} && $textconv ne {}} { + return 1 + } else { + return 0 + } +} + +proc popup_diff_menu {ctxm ctxmmg ctxmsm x y X Y} { + global current_diff_path file_states last_revert + set ::cursorX $x + set ::cursorY $y + if {[info exists file_states($current_diff_path)]} { + set state [lindex $file_states($current_diff_path) 0] + } else { + set state {__} + } + if {[string first {U} $state] >= 0} { + tk_popup $ctxmmg $X $Y + } elseif {$::is_submodule_diff} { + tk_popup $ctxmsm $X $Y + } else { + set has_range [expr {[$::ui_diff tag nextrange sel 0.0] != {}}] + set u [mc "Undo Last Revert"] + if {$::ui_index eq $::current_diff_side} { + set l [mc "Unstage Hunk From Commit"] + set h [mc "Revert Hunk"] + + if {$has_range} { + set t [mc "Unstage Lines From Commit"] + set r [mc "Revert Lines"] + } else { + set t [mc "Unstage Line From Commit"] + set r [mc "Revert Line"] + } + } else { + set l [mc "Stage Hunk For Commit"] + set h [mc "Revert Hunk"] + + if {$has_range} { + set t [mc "Stage Lines For Commit"] + set r [mc "Revert Lines"] + } else { + set t [mc "Stage Line For Commit"] + set r [mc "Revert Line"] + } + } + if {$::is_3way_diff + || $current_diff_path eq {} + || {__} eq $state + || {_O} eq $state + || [string match {?T} $state] + || [string match {T?} $state] + || [has_textconv $current_diff_path]} { + set s disabled + set revert_state disabled + } else { + set s normal + + # Only allow reverting changes in the working tree. If + # the user wants to revert changes in the index, they + # need to unstage those first. + if {$::ui_workdir eq $::current_diff_side} { + set revert_state normal + } else { + set revert_state disabled + } + } + + if {$last_revert eq {}} { + set undo_state disabled + } else { + set undo_state normal + } + + $ctxm entryconf $::ui_diff_applyhunk -state $s -label $l + $ctxm entryconf $::ui_diff_applyline -state $s -label $t + $ctxm entryconf $::ui_diff_revertline -state $revert_state \ + -label $r + $ctxm entryconf $::ui_diff_reverthunk -state $revert_state \ + -label $h + $ctxm entryconf $::ui_diff_undorevert -state $undo_state \ + -label $u + + tk_popup $ctxm $X $Y + } +} +bind_button3 $ui_diff [list popup_diff_menu $ctxm $ctxmmg $ctxmsm %x %y %X %Y] + +# -- Status Bar +# +set main_status [::status_bar::new .status] +pack .status -anchor w -side bottom -fill x +$main_status show [mc "Initializing..."] + +# -- Load geometry +# +proc on_ttk_pane_mapped {w pane pos} { + bind $w <Map> {} + after 0 [list after idle [list $w sashpos $pane $pos]] +} +proc on_tk_pane_mapped {w pane x y} { + bind $w <Map> {} + after 0 [list after idle [list $w sash place $pane $x $y]] +} +proc on_application_mapped {} { + global repo_config use_ttk + bind . <Map> {} + set gm $repo_config(gui.geometry) + if {$use_ttk} { + bind .vpane <Map> \ + [list on_ttk_pane_mapped %W 0 [lindex $gm 1]] + bind .vpane.files <Map> \ + [list on_ttk_pane_mapped %W 0 [lindex $gm 2]] + } else { + bind .vpane <Map> \ + [list on_tk_pane_mapped %W 0 \ + [lindex $gm 1] \ + [lindex [.vpane sash coord 0] 1]] + bind .vpane.files <Map> \ + [list on_tk_pane_mapped %W 0 \ + [lindex [.vpane.files sash coord 0] 0] \ + [lindex $gm 2]] + } + wm geometry . [lindex $gm 0] +} +if {[info exists repo_config(gui.geometry)]} { + bind . <Map> [list on_application_mapped] + wm geometry . [lindex $repo_config(gui.geometry) 0] +} + +# -- Load window state +# +if {[info exists repo_config(gui.wmstate)]} { + catch {wm state . $repo_config(gui.wmstate)} +} + +# -- Key Bindings +# +bind $ui_comm <$M1B-Key-Return> {do_commit;break} +bind $ui_comm <$M1B-Key-t> {do_add_selection;break} +bind $ui_comm <$M1B-Key-T> {do_add_selection;break} +bind $ui_comm <$M1B-Key-u> {do_unstage_selection;break} +bind $ui_comm <$M1B-Key-U> {do_unstage_selection;break} +bind $ui_comm <$M1B-Key-j> {do_revert_selection;break} +bind $ui_comm <$M1B-Key-J> {do_revert_selection;break} +bind $ui_comm <$M1B-Key-i> {do_add_all;break} +bind $ui_comm <$M1B-Key-I> {do_add_all;break} +bind $ui_comm <$M1B-Key-x> {tk_textCut %W;break} +bind $ui_comm <$M1B-Key-X> {tk_textCut %W;break} +bind $ui_comm <$M1B-Key-c> {tk_textCopy %W;break} +bind $ui_comm <$M1B-Key-C> {tk_textCopy %W;break} +bind $ui_comm <$M1B-Key-v> {tk_textPaste %W; %W see insert; break} +bind $ui_comm <$M1B-Key-V> {tk_textPaste %W; %W see insert; break} +bind $ui_comm <$M1B-Key-a> {%W tag add sel 0.0 end;break} +bind $ui_comm <$M1B-Key-A> {%W tag add sel 0.0 end;break} +bind $ui_comm <$M1B-Key-minus> {show_less_context;break} +bind $ui_comm <$M1B-Key-KP_Subtract> {show_less_context;break} +bind $ui_comm <$M1B-Key-equal> {show_more_context;break} +bind $ui_comm <$M1B-Key-plus> {show_more_context;break} +bind $ui_comm <$M1B-Key-KP_Add> {show_more_context;break} + +bind $ui_diff <$M1B-Key-x> {tk_textCopy %W;break} +bind $ui_diff <$M1B-Key-X> {tk_textCopy %W;break} +bind $ui_diff <$M1B-Key-c> {tk_textCopy %W;break} +bind $ui_diff <$M1B-Key-C> {tk_textCopy %W;break} +bind $ui_diff <$M1B-Key-v> {break} +bind $ui_diff <$M1B-Key-V> {break} +bind $ui_diff <$M1B-Key-a> {%W tag add sel 0.0 end;break} +bind $ui_diff <$M1B-Key-A> {%W tag add sel 0.0 end;break} +bind $ui_diff <$M1B-Key-j> {do_revert_selection;break} +bind $ui_diff <$M1B-Key-J> {do_revert_selection;break} +bind $ui_diff <Key-Up> {catch {%W yview scroll -1 units};break} +bind $ui_diff <Key-Down> {catch {%W yview scroll 1 units};break} +bind $ui_diff <Key-Left> {catch {%W xview scroll -1 units};break} +bind $ui_diff <Key-Right> {catch {%W xview scroll 1 units};break} +bind $ui_diff <Key-k> {catch {%W yview scroll -1 units};break} +bind $ui_diff <Key-j> {catch {%W yview scroll 1 units};break} +bind $ui_diff <Key-h> {catch {%W xview scroll -1 units};break} +bind $ui_diff <Key-l> {catch {%W xview scroll 1 units};break} +bind $ui_diff <Control-Key-b> {catch {%W yview scroll -1 pages};break} +bind $ui_diff <Control-Key-f> {catch {%W yview scroll 1 pages};break} +bind $ui_diff <Button-1> {focus %W} + +if {[is_enabled branch]} { + bind . <$M1B-Key-n> branch_create::dialog + bind . <$M1B-Key-N> branch_create::dialog + bind . <$M1B-Key-o> branch_checkout::dialog + bind . <$M1B-Key-O> branch_checkout::dialog + bind . <$M1B-Key-m> merge::dialog + bind . <$M1B-Key-M> merge::dialog +} +if {[is_enabled transport]} { + bind . <$M1B-Key-p> do_push_anywhere + bind . <$M1B-Key-P> do_push_anywhere +} + +bind . <Key-F5> ui_do_rescan +bind . <$M1B-Key-r> ui_do_rescan +bind . <$M1B-Key-R> ui_do_rescan +bind . <$M1B-Key-s> do_signoff +bind . <$M1B-Key-S> do_signoff +bind . <$M1B-Key-t> { toggle_or_diff toggle %W } +bind . <$M1B-Key-T> { toggle_or_diff toggle %W } +bind . <$M1B-Key-u> { toggle_or_diff toggle %W } +bind . <$M1B-Key-U> { toggle_or_diff toggle %W } +bind . <$M1B-Key-j> do_revert_selection +bind . <$M1B-Key-J> do_revert_selection +bind . <$M1B-Key-i> do_add_all +bind . <$M1B-Key-I> do_add_all +bind . <$M1B-Key-e> toggle_commit_type +bind . <$M1B-Key-E> toggle_commit_type +bind . <$M1B-Key-minus> {show_less_context;break} +bind . <$M1B-Key-KP_Subtract> {show_less_context;break} +bind . <$M1B-Key-equal> {show_more_context;break} +bind . <$M1B-Key-plus> {show_more_context;break} +bind . <$M1B-Key-KP_Add> {show_more_context;break} +bind . <$M1B-Key-Return> do_commit +bind . <$M1B-Key-KP_Enter> do_commit +foreach i [list $ui_index $ui_workdir] { + bind $i <Button-1> { toggle_or_diff click %W %x %y; break } + bind $i <$M1B-Button-1> { add_one_to_selection %W %x %y; break } + bind $i <Shift-Button-1> { add_range_to_selection %W %x %y; break } + bind $i <Key-Up> { toggle_or_diff up %W; break } + bind $i <Key-Down> { toggle_or_diff down %W; break } +} +unset i + +bind . <Alt-Key-1> {focus_widget $::ui_workdir} +bind . <Alt-Key-2> {focus_widget $::ui_index} +bind . <Alt-Key-3> {focus $::ui_diff} +bind . <Alt-Key-4> {focus $::ui_comm} + +set file_lists_last_clicked($ui_index) {} +set file_lists_last_clicked($ui_workdir) {} + +set file_lists($ui_index) [list] +set file_lists($ui_workdir) [list] + +wm title . "[appname] ([reponame]) [file normalize $_gitworktree]" +focus -force $ui_comm + +# -- Warn the user about environmental problems. Cygwin's Tcl +# does *not* pass its env array onto any processes it spawns. +# This means that git processes get none of our environment. +# +if {[is_Cygwin]} { + set ignored_env 0 + set suggest_user {} + set msg [mc "Possible environment issues exist. + +The following environment variables are probably +going to be ignored by any Git subprocess run +by %s: + +" [appname]] + foreach name [array names env] { + switch -regexp -- $name { + {^GIT_INDEX_FILE$} - + {^GIT_OBJECT_DIRECTORY$} - + {^GIT_ALTERNATE_OBJECT_DIRECTORIES$} - + {^GIT_DIFF_OPTS$} - + {^GIT_EXTERNAL_DIFF$} - + {^GIT_PAGER$} - + {^GIT_TRACE$} - + {^GIT_CONFIG$} - + {^GIT_(AUTHOR|COMMITTER)_DATE$} { + append msg " - $name\n" + incr ignored_env + } + {^GIT_(AUTHOR|COMMITTER)_(NAME|EMAIL)$} { + append msg " - $name\n" + incr ignored_env + set suggest_user $name + } + } + } + if {$ignored_env > 0} { + append msg [mc " +This is due to a known issue with the +Tcl binary distributed by Cygwin."] + + if {$suggest_user ne {}} { + append msg [mc " + +A good replacement for %s +is placing values for the user.name and +user.email settings into your personal +~/.gitconfig file. +" $suggest_user] + } + warn_popup $msg + } + unset ignored_env msg suggest_user name +} + +# -- Only initialize complex UI if we are going to stay running. +# +if {[is_enabled transport]} { + load_all_remotes + + set n [.mbar.remote index end] + populate_remotes_menu + set n [expr {[.mbar.remote index end] - $n}] + if {$n > 0} { + if {[.mbar.remote type 0] eq "tearoff"} { incr n } + .mbar.remote insert $n separator + } + unset n +} + +if {[winfo exists $ui_comm]} { + set GITGUI_BCK_exists [load_message GITGUI_BCK utf-8] + + # -- If both our backup and message files exist use the + # newer of the two files to initialize the buffer. + # + if {$GITGUI_BCK_exists} { + set m [gitdir GITGUI_MSG] + if {[file isfile $m]} { + if {[file mtime [gitdir GITGUI_BCK]] > [file mtime $m]} { + catch {file delete [gitdir GITGUI_MSG]} + } else { + $ui_comm delete 0.0 end + $ui_comm edit reset + $ui_comm edit modified false + catch {file delete [gitdir GITGUI_BCK]} + set GITGUI_BCK_exists 0 + } + } + unset m + } + + proc backup_commit_buffer {} { + global ui_comm GITGUI_BCK_exists + + set m [$ui_comm edit modified] + if {$m || $GITGUI_BCK_exists} { + set msg [string trim [$ui_comm get 0.0 end]] + regsub -all -line {[ \r\t]+$} $msg {} msg + + if {$msg eq {}} { + if {$GITGUI_BCK_exists} { + catch {file delete [gitdir GITGUI_BCK]} + set GITGUI_BCK_exists 0 + } + } elseif {$m} { + catch { + set fd [open [gitdir GITGUI_BCK] w] + fconfigure $fd -encoding utf-8 + puts -nonewline $fd $msg + close $fd + set GITGUI_BCK_exists 1 + } + } + + $ui_comm edit modified false + } + + set ::GITGUI_BCK_i [after 2000 backup_commit_buffer] + } + + backup_commit_buffer + + # -- If the user has aspell available we can drive it + # in pipe mode to spellcheck the commit message. + # + set spell_cmd [list |] + set spell_dict [get_config gui.spellingdictionary] + lappend spell_cmd aspell + if {$spell_dict ne {}} { + lappend spell_cmd --master=$spell_dict + } + lappend spell_cmd --mode=none + lappend spell_cmd --encoding=utf-8 + lappend spell_cmd pipe + if {$spell_dict eq {none} + || [catch {set spell_fd [open $spell_cmd r+]} spell_err]} { + bind_button3 $ui_comm [list tk_popup $ui_comm_ctxm %X %Y] + } else { + set ui_comm_spell [spellcheck::init \ + $spell_fd \ + $ui_comm \ + $ui_comm_ctxm \ + ] + } + unset -nocomplain spell_cmd spell_fd spell_err spell_dict +} + +lock_index begin-read +if {![winfo ismapped .]} { + wm deiconify . +} +after 1 { + if {[is_enabled initialamend]} { + force_amend + } else { + do_rescan + } + + if {[is_enabled nocommitmsg]} { + $ui_comm configure -state disabled -background gray + } +} +if {[is_enabled multicommit] && ![is_config_false gui.gcwarning]} { + after 1000 hint_gc +} +if {[is_enabled retcode]} { + bind . <Destroy> {+terminate_me %W} +} +if {$picked && [is_config_true gui.autoexplore]} { + do_explore +} + +# Clear "Initializing..." status +after 500 {$main_status show ""} + +# Local variables: +# mode: tcl +# indent-tabs-mode: t +# tab-width: 4 +# End: diff --git a/git-gui/lib/about.tcl b/git-gui/lib/about.tcl new file mode 100644 index 0000000..cfa50fc --- /dev/null +++ b/git-gui/lib/about.tcl @@ -0,0 +1,70 @@ +# git-gui about git-gui dialog +# Copyright (C) 2006, 2007 Shawn Pearce + +proc do_about {} { + global appvers copyright oguilib + global tcl_patchLevel tk_patchLevel + global ui_comm_spell NS use_ttk + + set w .about_dialog + Dialog $w + wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + + pack [git_logo $w.git_logo] -side left -fill y -padx 10 -pady 10 + ${NS}::label $w.header -text [mc "About %s" [appname]] \ + -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.close -text {Close} \ + -default active \ + -command [list destroy $w] + pack $w.buttons.close -side right + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + paddedlabel $w.desc \ + -text "[mc "git-gui - a graphical user interface for Git."]\n$copyright" + pack $w.desc -side top -fill x -padx 5 -pady 5 + + set v {} + append v "git-gui version $appvers\n" + append v "[git version]\n" + append v "\n" + if {$tcl_patchLevel eq $tk_patchLevel} { + append v "Tcl/Tk version $tcl_patchLevel" + } else { + append v "Tcl version $tcl_patchLevel" + append v ", Tk version $tk_patchLevel" + } + if {[info exists ui_comm_spell] + && [$ui_comm_spell version] ne {}} { + append v "\n" + append v [$ui_comm_spell version] + } + + set d {} + append d "git wrapper: $::_git\n" + append d "git exec dir: [gitexec]\n" + append d "git-gui lib: $oguilib" + + paddedlabel $w.vers -text $v + pack $w.vers -side top -fill x -padx 5 -pady 5 + + paddedlabel $w.dirs -text $d + pack $w.dirs -side top -fill x -padx 5 -pady 5 + + menu $w.ctxm -tearoff 0 + $w.ctxm add command \ + -label {Copy} \ + -command " + clipboard clear + clipboard append -format STRING -type STRING -- \[$w.vers cget -text\] + " + + bind $w <Visibility> "grab $w; focus $w.buttons.close" + bind $w <Key-Escape> "destroy $w" + bind $w <Key-Return> "destroy $w" + bind_button3 $w.vers "tk_popup $w.ctxm %X %Y; grab $w; focus $w" + wm title $w "About [appname]" + tkwait window $w +} diff --git a/git-gui/lib/blame.tcl b/git-gui/lib/blame.tcl new file mode 100644 index 0000000..62ec083 --- /dev/null +++ b/git-gui/lib/blame.tcl @@ -0,0 +1,1373 @@ +# git-gui blame viewer +# Copyright (C) 2006, 2007 Shawn Pearce + +class blame { + +image create photo ::blame::img_back_arrow -data {R0lGODlhGAAYAIUAAPwCBEzKXFTSZIz+nGzmhGzqfGTidIT+nEzGXHTqhGzmfGzifFzadETCVES+VARWDFzWbHzyjAReDGTadFTOZDSyRDyyTCymPARaFGTedFzSbDy2TCyqRCyqPARaDAyCHES6VDy6VCyiPAR6HCSeNByWLARyFARiDARqFGTifARiFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAYABgAAAajQIBwSCwaj8ikcsk0BppJwRPqHEypQwHBis0WDAdEFyBIKBaMAKLBdjQeSkFBYTBAIvgEoS6JmhUTEwIUDQ4VFhcMGEhyCgoZExoUaxsWHB0THkgfAXUGAhoBDSAVFR0XBnCbDRmgog0hpSIiDJpJIyEQhBUcJCIlwA22SSYVogknEg8eD82qSigdDSknY0IqJQXPYxIl1dZCGNvWw+Dm510GQQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} + +# Persistent data (survives loads) +# +field history {}; # viewer history: {commit path} +field header ; # array commit,key -> header field + +# Tk UI control paths +# +field w ; # top window in this viewer +field w_back ; # our back button +field w_path ; # label showing the current file path +field w_columns ; # list of all column widgets in the viewer +field w_line ; # text column: all line numbers +field w_amov ; # text column: annotations + move tracking +field w_asim ; # text column: annotations (simple computation) +field w_file ; # text column: actual file data +field w_cviewer ; # pane showing commit message +field finder ; # find mini-dialog frame +field gotoline ; # line goto mini-dialog frame +field status ; # status mega-widget instance +field status_operation ; # operation displayed by status mega-widget +field old_height ; # last known height of $w.file_pane + + +# Tk UI colors +# +variable active_color #c0edc5 +variable group_colors { + #d6d6d6 + #e1e1e1 + #ececec +} + +# Current blame data; cleared/reset on each load +# +field commit ; # input commit to blame +field path ; # input filename to view in $commit + +field current_fd {} ; # background process running +field highlight_line -1 ; # current line selected +field highlight_column {} ; # current commit column selected +field highlight_commit {} ; # sha1 of commit selected + +field total_lines 0 ; # total length of file +field blame_lines 0 ; # number of lines computed +field amov_data ; # list of {commit origfile origline} +field asim_data ; # list of {commit origfile origline} + +field r_commit ; # commit currently being parsed +field r_orig_line ; # original line number +field r_final_line ; # final line number +field r_line_count ; # lines in this region + +field tooltip_wm {} ; # Current tooltip toplevel, if open +field tooltip_t {} ; # Text widget in $tooltip_wm +field tooltip_timer {} ; # Current timer event for our tooltip +field tooltip_commit {} ; # Commit(s) in tooltip + +constructor new {i_commit i_path i_jump} { + global cursor_ptr M1B M1T have_tk85 use_ttk NS + variable active_color + variable group_colors + + set commit $i_commit + set path $i_path + + make_toplevel top w + wm title $top [mc "%s (%s): File Viewer" [appname] [reponame]] + + set font_w [font measure font_diff "0"] + + gold_frame $w.header + tlabel $w.header.commit_l \ + -text [mc "Commit:"] \ + -background gold \ + -foreground black \ + -anchor w \ + -justify left + set w_back $w.header.commit_b + tlabel $w_back \ + -image ::blame::img_back_arrow \ + -borderwidth 0 \ + -relief flat \ + -state disabled \ + -background gold \ + -foreground black \ + -activebackground gold + bind $w_back <Button-1> " + if {\[$w_back cget -state\] eq {normal}} { + [cb _history_menu] + } + " + tlabel $w.header.commit \ + -textvariable @commit \ + -background gold \ + -foreground black \ + -anchor w \ + -justify left + tlabel $w.header.path_l \ + -text [mc "File:"] \ + -background gold \ + -foreground black \ + -anchor w \ + -justify left + set w_path $w.header.path + tlabel $w_path \ + -background gold \ + -foreground black \ + -anchor w \ + -justify left + pack $w.header.commit_l -side left + pack $w_back -side left + pack $w.header.commit -side left + pack $w_path -fill x -side right + pack $w.header.path_l -side right + + panedwindow $w.file_pane -orient vertical -borderwidth 0 -sashwidth 3 + frame $w.file_pane.out -relief flat -borderwidth 1 + frame $w.file_pane.cm -relief sunken -borderwidth 1 + $w.file_pane add $w.file_pane.out \ + -sticky nsew \ + -minsize 100 \ + -height 100 \ + -width 100 + $w.file_pane add $w.file_pane.cm \ + -sticky nsew \ + -minsize 25 \ + -height 25 \ + -width 100 + + set w_line $w.file_pane.out.linenumber_t + text $w_line \ + -takefocus 0 \ + -highlightthickness 0 \ + -padx 0 -pady 0 \ + -background white \ + -foreground black \ + -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 6 \ + -font font_diff + $w_line tag conf linenumber -justify right -rmargin 5 + + set w_amov $w.file_pane.out.amove_t + text $w_amov \ + -takefocus 0 \ + -highlightthickness 0 \ + -padx 0 -pady 0 \ + -background white \ + -foreground black \ + -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 5 \ + -font font_diff + $w_amov tag conf author_abbr -justify right -rmargin 5 + $w_amov tag conf curr_commit + $w_amov tag conf prior_commit -foreground blue -underline 1 + $w_amov tag bind prior_commit \ + <Button-1> \ + "[cb _load_commit $w_amov @amov_data @%x,%y];break" + + set w_asim $w.file_pane.out.asimple_t + text $w_asim \ + -takefocus 0 \ + -highlightthickness 0 \ + -padx 0 -pady 0 \ + -background white \ + -foreground black \ + -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 4 \ + -font font_diff + $w_asim tag conf author_abbr -justify right + $w_asim tag conf curr_commit + $w_asim tag conf prior_commit -foreground blue -underline 1 + $w_asim tag bind prior_commit \ + <Button-1> \ + "[cb _load_commit $w_asim @asim_data @%x,%y];break" + + set w_file $w.file_pane.out.file_t + text $w_file \ + -takefocus 0 \ + -highlightthickness 0 \ + -padx 0 -pady 0 \ + -background white \ + -foreground black \ + -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 40 \ + -width 80 \ + -xscrollcommand [list $w.file_pane.out.sbx set] \ + -font font_diff + if {$have_tk85} { + $w_file configure -inactiveselectbackground darkblue + } + $w_file tag conf found \ + -background yellow + + set w_columns [list $w_amov $w_asim $w_line $w_file] + + ${NS}::scrollbar $w.file_pane.out.sbx \ + -orient h \ + -command [list $w_file xview] + ${NS}::scrollbar $w.file_pane.out.sby \ + -orient v \ + -command [list scrollbar2many $w_columns yview] + eval grid $w_columns $w.file_pane.out.sby -sticky nsew + grid conf \ + $w.file_pane.out.sbx \ + -column 0 \ + -columnspan [expr {[llength $w_columns] + 1}] \ + -sticky we + grid columnconfigure \ + $w.file_pane.out \ + [expr {[llength $w_columns] - 1}] \ + -weight 1 + grid rowconfigure $w.file_pane.out 0 -weight 1 + + set finder [::searchbar::new \ + $w.file_pane.out.ff $w_file \ + -column 0 \ + -columnspan [expr {[llength $w_columns] + 1}] \ + ] + + set gotoline [::linebar::new \ + $w.file_pane.out.lf $w_file \ + -column 0 \ + -columnspan [expr {[llength $w_columns] + 1}] \ + ] + + set w_cviewer $w.file_pane.cm.t + text $w_cviewer \ + -background white \ + -foreground black \ + -borderwidth 0 \ + -state disabled \ + -wrap none \ + -height 10 \ + -width 80 \ + -xscrollcommand [list $w.file_pane.cm.sbx set] \ + -yscrollcommand [list $w.file_pane.cm.sby set] \ + -font font_diff + $w_cviewer tag conf still_loading \ + -font font_uiitalic \ + -justify center + $w_cviewer tag conf header_key \ + -tabs {3c} \ + -background $active_color \ + -font font_uibold + $w_cviewer tag conf header_val \ + -background $active_color \ + -font font_ui + $w_cviewer tag raise sel + ${NS}::scrollbar $w.file_pane.cm.sbx \ + -orient h \ + -command [list $w_cviewer xview] + ${NS}::scrollbar $w.file_pane.cm.sby \ + -orient v \ + -command [list $w_cviewer yview] + pack $w.file_pane.cm.sby -side right -fill y + pack $w.file_pane.cm.sbx -side bottom -fill x + pack $w_cviewer -expand 1 -fill both + + set status [::status_bar::new $w.status] + set status_operation {} + + menu $w.ctxm -tearoff 0 + $w.ctxm add command \ + -label [mc "Copy Commit"] \ + -command [cb _copycommit] + $w.ctxm add separator + $w.ctxm add command \ + -label [mc "Find Text..."] \ + -accelerator F7 \ + -command [cb _show_finder] + $w.ctxm add command \ + -label [mc "Goto Line..."] \ + -accelerator "Ctrl-G" \ + -command [cb _show_linebar] + menu $w.ctxm.enc + build_encoding_menu $w.ctxm.enc [cb _setencoding] + $w.ctxm add cascade \ + -label [mc "Encoding"] \ + -menu $w.ctxm.enc + $w.ctxm add command \ + -label [mc "Do Full Copy Detection"] \ + -command [cb _fullcopyblame] + $w.ctxm add separator + $w.ctxm add command \ + -label [mc "Show History Context"] \ + -command [cb _gitkcommit] + $w.ctxm add command \ + -label [mc "Blame Parent Commit"] \ + -command [cb _blameparent] + + foreach i $w_columns { + for {set g 0} {$g < [llength $group_colors]} {incr g} { + $i tag conf color$g -background [lindex $group_colors $g] + } + + if {$i eq $w_file} { + $w_file tag raise found + } + $i tag raise sel + + $i conf -cursor $cursor_ptr + $i conf -yscrollcommand \ + "[list ::searchbar::scrolled $finder] + [list many2scrollbar $w_columns yview $w.file_pane.out.sby]" + bind $i <Button-1> " + [cb _hide_tooltip] + [cb _click $i @%x,%y] + focus $i + " + bind $i <Any-Motion> [cb _show_tooltip $i @%x,%y] + bind $i <Any-Enter> [cb _hide_tooltip] + bind $i <Any-Leave> [cb _hide_tooltip] + bind_button3 $i " + [cb _hide_tooltip] + set cursorX %x + set cursorY %y + set cursorW %W + tk_popup $w.ctxm %X %Y + " + bind $i <Shift-Tab> "[list focus $w_cviewer];break" + bind $i <Tab> "[cb _focus_search $w_cviewer];break" + } + + foreach i [concat $w_columns $w_cviewer] { + bind $i <Key-Up> {catch {%W yview scroll -1 units};break} + bind $i <Key-Down> {catch {%W yview scroll 1 units};break} + bind $i <Key-Left> {catch {%W xview scroll -1 units};break} + bind $i <Key-Right> {catch {%W xview scroll 1 units};break} + bind $i <Key-k> {catch {%W yview scroll -1 units};break} + bind $i <Key-j> {catch {%W yview scroll 1 units};break} + bind $i <Key-h> {catch {%W xview scroll -1 units};break} + bind $i <Key-l> {catch {%W xview scroll 1 units};break} + bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break} + bind $i <Control-Key-f> {catch {%W yview scroll 1 pages};break} + } + + bind $w_cviewer <Shift-Tab> "[cb _focus_search $w_file];break" + bind $w_cviewer <Tab> "[list focus $w_file];break" + bind $w_cviewer <Button-1> [list focus $w_cviewer] + bind $w_file <Visibility> [cb _focus_search $w_file] + bind $top <F7> [cb _show_finder] + bind $top <Key-slash> [cb _show_finder] + bind $top <Control-Key-s> [cb _show_finder] + bind $top <Escape> [list searchbar::hide $finder] + bind $top <F3> [list searchbar::find_next $finder] + bind $top <Shift-F3> [list searchbar::find_prev $finder] + bind $top <Control-Key-g> [cb _show_linebar] + catch { bind $top <Shift-Key-XF86_Switch_VT_3> [list searchbar::find_prev $finder] } + + grid configure $w.header -sticky ew + grid configure $w.file_pane -sticky nsew + grid configure $w.status -sticky ew + grid columnconfigure $top 0 -weight 1 + grid rowconfigure $top 0 -weight 0 + grid rowconfigure $top 1 -weight 1 + grid rowconfigure $top 2 -weight 0 + + set req_w [winfo reqwidth $top] + set req_h [winfo reqheight $top] + set scr_w [expr {[winfo screenwidth $top] - 40}] + set scr_h [expr {[winfo screenheight $top] - 120}] + set opt_w [expr {$font_w * (80 + 5*3 + 3)}] + if {$req_w < $opt_w} {set req_w $opt_w} + if {$req_w > $scr_w} {set req_w $scr_w} + set opt_h [expr {$req_w*4/3}] + if {$req_h < $scr_h} {set req_h $scr_h} + if {$req_h > $opt_h} {set req_h $opt_h} + set g "${req_w}x${req_h}" + wm geometry $top $g + update + + set old_height [winfo height $w.file_pane] + $w.file_pane sash place 0 \ + [lindex [$w.file_pane sash coord 0] 0] \ + [expr {int($old_height * 0.80)}] + bind $w.file_pane <Configure> \ + "if {{$w.file_pane} eq {%W}} {[cb _resize %h]}" + + wm protocol $top WM_DELETE_WINDOW "destroy $top" + bind $top <Destroy> [cb _handle_destroy %W] + + _load $this $i_jump +} + +method _focus_search {win} { + if {[searchbar::visible $finder]} { + focus [searchbar::editor $finder] + } else { + focus $win + } +} + +method _handle_destroy {win} { + if {$win eq $w} { + _kill $this + delete_this + } +} + +method _kill {} { + if {$current_fd ne {}} { + kill_file_process $current_fd + catch {close $current_fd} + set current_fd {} + } +} + +method _load {jump} { + variable group_colors + + _hide_tooltip $this + + if {$total_lines != 0 || $current_fd ne {}} { + _kill $this + + foreach i $w_columns { + $i conf -state normal + $i delete 0.0 end + foreach g [$i tag names] { + if {[regexp {^g[0-9a-f]{40}$} $g]} { + $i tag delete $g + } + } + $i conf -state disabled + } + + $w_cviewer conf -state normal + $w_cviewer delete 0.0 end + $w_cviewer conf -state disabled + + set highlight_line -1 + set highlight_column {} + set highlight_commit {} + set total_lines 0 + } + + if {$history eq {}} { + $w_back conf -state disabled + } else { + $w_back conf -state normal + } + + # Index 0 is always empty. There is never line 0 as + # we use only 1 based lines, as that matches both with + # git-blame output and with Tk's text widget. + # + set amov_data [list [list]] + set asim_data [list [list]] + + $status show [mc "Reading %s..." "$commit:[escape_path $path]"] + $w_path conf -text [escape_path $path] + + set do_textconv 0 + if {![is_config_false gui.textconv] && [git-version >= 1.7.2]} { + set filter [gitattr $path diff set] + set textconv [get_config [join [list diff $filter textconv] .]] + if {$filter ne {set} && $textconv ne {}} { + set do_textconv 1 + } + } + if {$commit eq {}} { + if {$do_textconv ne 0} { + set fd [open_cmd_pipe $textconv $path] + } else { + set fd [open $path r] + } + fconfigure $fd -eofchar {} + } else { + if {$do_textconv ne 0} { + set fd [git_read cat-file --textconv "$commit:$path"] + } else { + set fd [git_read cat-file blob "$commit:$path"] + } + } + fconfigure $fd \ + -blocking 0 \ + -translation lf \ + -encoding [get_path_encoding $path] + fileevent $fd readable [cb _read_file $fd $jump] + set current_fd $fd +} + +method _history_menu {} { + set m $w.backmenu + if {[winfo exists $m]} { + $m delete 0 end + } else { + menu $m -tearoff 0 + } + + for {set i [expr {[llength $history] - 1}] + } {$i >= 0} {incr i -1} { + set e [lindex $history $i] + set c [lindex $e 0] + set f [lindex $e 1] + + if {[regexp {^[0-9a-f]{40}$} $c]} { + set t [string range $c 0 8]... + } elseif {$c eq {}} { + set t {Working Directory} + } else { + set t $c + } + if {![catch {set summary $header($c,summary)}]} { + append t " $summary" + if {[string length $t] > 70} { + set t [string range $t 0 66]... + } + } + + $m add command -label $t -command [cb _goback $i] + } + set X [winfo rootx $w_back] + set Y [expr {[winfo rooty $w_back] + [winfo height $w_back]}] + tk_popup $m $X $Y +} + +method _goback {i} { + set dat [lindex $history $i] + set history [lrange $history 0 [expr {$i - 1}]] + set commit [lindex $dat 0] + set path [lindex $dat 1] + _load $this [lrange $dat 2 5] +} + +method _read_file {fd jump} { + if {$fd ne $current_fd} { + catch {close $fd} + return + } + + foreach i $w_columns {$i conf -state normal} + while {[gets $fd line] >= 0} { + regsub "\r\$" $line {} line + incr total_lines + lappend amov_data {} + lappend asim_data {} + + if {$total_lines > 1} { + foreach i $w_columns {$i insert end "\n"} + } + + $w_line insert end "$total_lines" linenumber + $w_file insert end "$line" + } + + set ln_wc [expr {[string length $total_lines] + 2}] + if {[$w_line cget -width] < $ln_wc} { + $w_line conf -width $ln_wc + } + + foreach i $w_columns {$i conf -state disabled} + + if {[eof $fd]} { + fconfigure $fd -blocking 1; # enable error reporting on close + if {[catch {close $fd} err]} { + tk_messageBox -icon error -title [mc Error] \ + -message $err + } + + # If we don't force Tk to update the widgets *right now* + # none of our jump commands will cause a change in the UI. + # + update + + if {[llength $jump] == 1} { + set highlight_line [lindex $jump 0] + $w_file see "$highlight_line.0" + } elseif {[llength $jump] == 4} { + set highlight_column [lindex $jump 0] + set highlight_line [lindex $jump 1] + $w_file xview moveto [lindex $jump 2] + $w_file yview moveto [lindex $jump 3] + } + + _exec_blame $this $w_asim @asim_data \ + [list] \ + [mc "Loading copy/move tracking annotations..."] + } +} ifdeleted { catch {close $fd} } + +method _exec_blame {cur_w cur_d options cur_s} { + lappend options --incremental --encoding=utf-8 + if {$commit eq {}} { + lappend options --contents $path + } else { + lappend options $commit + } + + # We may recurse in from another call to _exec_blame and already have + # a status operation. + if {$status_operation == {}} { + set status_operation [$status start \ + $cur_s \ + [mc "lines annotated"]] + } else { + $status_operation restart $cur_s + } + + lappend options -- $path + set fd [eval git_read --nice blame $options] + fconfigure $fd -blocking 0 -translation lf -encoding utf-8 + fileevent $fd readable [cb _read_blame $fd $cur_w $cur_d] + set current_fd $fd + set blame_lines 0 +} + +method _read_blame {fd cur_w cur_d} { + upvar #0 $cur_d line_data + variable group_colors + + if {$fd ne $current_fd} { + catch {close $fd} + return + } + + $cur_w conf -state normal + while {[gets $fd line] >= 0} { + if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \ + cmit original_line final_line line_count]} { + set r_commit $cmit + set r_orig_line $original_line + set r_final_line $final_line + set r_line_count $line_count + } elseif {[string match {filename *} $line]} { + set file [string range $line 9 end] + set n $r_line_count + set lno $r_final_line + set oln $r_orig_line + set cmit $r_commit + + if {[regexp {^0{40}$} $cmit]} { + set commit_abbr work + set commit_type curr_commit + } elseif {$cmit eq $commit} { + set commit_abbr this + set commit_type curr_commit + } else { + set commit_type prior_commit + set commit_abbr [string range $cmit 0 3] + } + + set author_abbr {} + set a_name {} + catch {set a_name $header($cmit,author)} + while {$a_name ne {}} { + if {$author_abbr ne {} + && [string index $a_name 0] eq {'}} { + regsub {^'[^']+'\s+} $a_name {} a_name + } + if {![regexp {^([[:upper:]])} $a_name _a]} break + append author_abbr $_a + unset _a + if {![regsub \ + {^[[:upper:]][^\s]*\s+} \ + $a_name {} a_name ]} break + } + if {$author_abbr eq {}} { + set author_abbr { |} + } else { + set author_abbr [string range $author_abbr 0 3] + } + unset a_name + + set first_lno $lno + while { + $first_lno > 1 + && $cmit eq [lindex $line_data [expr {$first_lno - 1}] 0] + && $file eq [lindex $line_data [expr {$first_lno - 1}] 1] + } { + incr first_lno -1 + } + + set color {} + if {$first_lno < $lno} { + foreach g [$w_file tag names $first_lno.0] { + if {[regexp {^color[0-9]+$} $g]} { + set color $g + break + } + } + } else { + set i [lsort [concat \ + [$w_file tag names "[expr {$first_lno - 1}].0"] \ + [$w_file tag names "[expr {$lno + $n}].0"] \ + ]] + for {set g 0} {$g < [llength $group_colors]} {incr g} { + if {[lsearch -sorted -exact $i color$g] == -1} { + set color color$g + break + } + } + } + if {$color eq {}} { + set color color0 + } + + while {$n > 0} { + set lno_e "$lno.0 lineend + 1c" + if {[lindex $line_data $lno] ne {}} { + set g [lindex $line_data $lno 0] + foreach i $w_columns { + $i tag remove g$g $lno.0 $lno_e + } + } + lset line_data $lno [list $cmit $file $oln] + + $cur_w delete $lno.0 "$lno.0 lineend" + if {$lno == $first_lno} { + $cur_w insert $lno.0 $commit_abbr $commit_type + } elseif {$lno == [expr {$first_lno + 1}]} { + $cur_w insert $lno.0 $author_abbr author_abbr + } else { + $cur_w insert $lno.0 { |} + } + + foreach i $w_columns { + if {$cur_w eq $w_amov} { + for {set g 0} \ + {$g < [llength $group_colors]} \ + {incr g} { + $i tag remove color$g $lno.0 $lno_e + } + $i tag add $color $lno.0 $lno_e + } + $i tag add g$cmit $lno.0 $lno_e + } + + if {$highlight_column eq $cur_w} { + if {$highlight_line == -1 + && [lindex [$w_file yview] 0] == 0} { + $w_file see $lno.0 + set highlight_line $lno + } + if {$highlight_line == $lno} { + _showcommit $this $cur_w $lno + } + } + + incr n -1 + incr lno + incr oln + incr blame_lines + } + + while { + $cmit eq [lindex $line_data $lno 0] + && $file eq [lindex $line_data $lno 1] + } { + $cur_w delete $lno.0 "$lno.0 lineend" + + if {$lno == $first_lno} { + $cur_w insert $lno.0 $commit_abbr $commit_type + } elseif {$lno == [expr {$first_lno + 1}]} { + $cur_w insert $lno.0 $author_abbr author_abbr + } else { + $cur_w insert $lno.0 { |} + } + + if {$cur_w eq $w_amov} { + foreach i $w_columns { + for {set g 0} \ + {$g < [llength $group_colors]} \ + {incr g} { + $i tag remove color$g $lno.0 $lno_e + } + $i tag add $color $lno.0 $lno_e + } + } + + incr lno + } + + } elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} { + set header($r_commit,$key) $data + } + } + $cur_w conf -state disabled + + if {[eof $fd]} { + close $fd + if {$cur_w eq $w_asim} { + # Switches for original location detection + set threshold [get_config gui.copyblamethreshold] + set original_options [list "-C$threshold"] + + if {![is_config_true gui.fastcopyblame]} { + # thorough copy search; insert before the threshold + set original_options [linsert $original_options 0 -C] + } + if {[git-version >= 1.5.3]} { + lappend original_options -w ; # ignore indentation changes + } + + _exec_blame $this $w_amov @amov_data \ + $original_options \ + [mc "Loading original location annotations..."] + } else { + set current_fd {} + $status_operation stop [mc "Annotation complete."] + set status_operation {} + } + } else { + $status_operation update $blame_lines $total_lines + } +} ifdeleted { catch {close $fd} } + +method _find_commit_bound {data_list start_idx delta} { + upvar #0 $data_list line_data + set pos $start_idx + set limit [expr {[llength $line_data] - 1}] + set base_commit [lindex $line_data $pos 0] + + while {$pos > 0 && $pos < $limit} { + set new_pos [expr {$pos + $delta}] + if {[lindex $line_data $new_pos 0] ne $base_commit} { + return $pos + } + + set pos $new_pos + } + + return $pos +} + +method _fullcopyblame {} { + if {$current_fd ne {}} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [mc "Busy"] \ + -message [mc "Annotation process is already running."] + + return + } + + # Switches for original location detection + set threshold [get_config gui.copyblamethreshold] + set original_options [list -C -C "-C$threshold"] + + if {[git-version >= 1.5.3]} { + lappend original_options -w ; # ignore indentation changes + } + + # Find the line range + set pos @$::cursorX,$::cursorY + set lno [lindex [split [$::cursorW index $pos] .] 0] + set min_amov_lno [_find_commit_bound $this @amov_data $lno -1] + set max_amov_lno [_find_commit_bound $this @amov_data $lno 1] + set min_asim_lno [_find_commit_bound $this @asim_data $lno -1] + set max_asim_lno [_find_commit_bound $this @asim_data $lno 1] + + if {$min_asim_lno < $min_amov_lno} { + set min_amov_lno $min_asim_lno + } + + if {$max_asim_lno > $max_amov_lno} { + set max_amov_lno $max_asim_lno + } + + lappend original_options -L "$min_amov_lno,$max_amov_lno" + + # Clear lines + for {set i $min_amov_lno} {$i <= $max_amov_lno} {incr i} { + lset amov_data $i [list ] + } + + # Start the back-end process + _exec_blame $this $w_amov @amov_data \ + $original_options \ + [mc "Running thorough copy detection..."] +} + +method _click {cur_w pos} { + set lno [lindex [split [$cur_w index $pos] .] 0] + _showcommit $this $cur_w $lno +} + +method _setencoding {enc} { + force_path_encoding $path $enc + _load $this [list \ + $highlight_column \ + $highlight_line \ + [lindex [$w_file xview] 0] \ + [lindex [$w_file yview] 0] \ + ] +} + +method _load_commit {cur_w cur_d pos} { + upvar #0 $cur_d line_data + set lno [lindex [split [$cur_w index $pos] .] 0] + set dat [lindex $line_data $lno] + if {$dat ne {}} { + _load_new_commit $this \ + [lindex $dat 0] \ + [lindex $dat 1] \ + [list [lindex $dat 2]] + } +} + +method _load_new_commit {new_commit new_path jump} { + lappend history [list \ + $commit $path \ + $highlight_column \ + $highlight_line \ + [lindex [$w_file xview] 0] \ + [lindex [$w_file yview] 0] \ + ] + + set commit $new_commit + set path $new_path + _load $this $jump +} + +method _showcommit {cur_w lno} { + global repo_config + variable active_color + + if {$highlight_commit ne {}} { + foreach i $w_columns { + $i tag conf g$highlight_commit -background {} + $i tag lower g$highlight_commit + } + } + + if {$cur_w eq $w_asim} { + set dat [lindex $asim_data $lno] + set highlight_column $w_asim + } else { + set dat [lindex $amov_data $lno] + set highlight_column $w_amov + } + + $w_cviewer conf -state normal + $w_cviewer delete 0.0 end + + if {$dat eq {}} { + set cmit {} + $w_cviewer insert end [mc "Loading annotation..."] still_loading + } else { + set cmit [lindex $dat 0] + set file [lindex $dat 1] + + foreach i $w_columns { + $i tag conf g$cmit -background $active_color + $i tag raise g$cmit + if {$i eq $w_file} { + $w_file tag raise found + } + $i tag raise sel + } + + set author_name {} + set author_email {} + set author_time {} + catch {set author_name $header($cmit,author)} + catch {set author_email $header($cmit,author-mail)} + catch {set author_time [format_date $header($cmit,author-time)]} + + set committer_name {} + set committer_email {} + set committer_time {} + catch {set committer_name $header($cmit,committer)} + catch {set committer_email $header($cmit,committer-mail)} + catch {set committer_time [format_date $header($cmit,committer-time)]} + + if {[catch {set msg $header($cmit,message)}]} { + set msg {} + catch { + set fd [git_read cat-file commit $cmit] + fconfigure $fd -encoding binary -translation lf + # By default commits are assumed to be in utf-8 + set enc utf-8 + while {[gets $fd line] > 0} { + if {[string match {encoding *} $line]} { + set enc [string tolower [string range $line 9 end]] + } + } + set msg [read $fd] + close $fd + + set enc [tcl_encoding $enc] + if {$enc ne {}} { + set msg [encoding convertfrom $enc $msg] + } + set msg [string trim $msg] + } + set header($cmit,message) $msg + } + + $w_cviewer insert end "commit $cmit\n" header_key + $w_cviewer insert end [strcat [mc "Author:"] "\t"] header_key + $w_cviewer insert end "$author_name $author_email" header_val + $w_cviewer insert end " $author_time\n" header_val + + $w_cviewer insert end [strcat [mc "Committer:"] "\t"] header_key + $w_cviewer insert end "$committer_name $committer_email" header_val + $w_cviewer insert end " $committer_time\n" header_val + + if {$file ne $path} { + $w_cviewer insert end [strcat [mc "Original File:"] "\t"] header_key + $w_cviewer insert end "[escape_path $file]\n" header_val + } + + $w_cviewer insert end "\n$msg" + } + $w_cviewer conf -state disabled + + set highlight_line $lno + set highlight_commit $cmit + + if {[lsearch -exact $tooltip_commit $highlight_commit] != -1} { + _hide_tooltip $this + } +} + +method _get_click_amov_info {} { + set pos @$::cursorX,$::cursorY + set lno [lindex [split [$::cursorW index $pos] .] 0] + return [lindex $amov_data $lno] +} + +method _copycommit {} { + set dat [_get_click_amov_info $this] + if {$dat ne {}} { + clipboard clear + clipboard append \ + -format STRING \ + -type STRING \ + -- [lindex $dat 0] + } +} + +method _format_offset_date {base offset} { + set exval [expr {$base + $offset*24*60*60}] + return [clock format $exval -format {%Y-%m-%d}] +} + +method _gitkcommit {} { + global nullid + + set dat [_get_click_amov_info $this] + if {$dat ne {}} { + set cmit [lindex $dat 0] + + # If the line belongs to the working copy, use HEAD instead + if {$cmit eq $nullid} { + if {[catch {set cmit [git rev-parse --verify HEAD]} err]} { + error_popup [strcat [mc "Cannot find HEAD commit:"] "\n\n$err"] + return; + } + } + + set radius [get_config gui.blamehistoryctx] + set cmdline [list --select-commit=$cmit] + + if {$radius > 0} { + set author_time {} + set committer_time {} + + catch {set author_time $header($cmit,author-time)} + catch {set committer_time $header($cmit,committer-time)} + + if {$committer_time eq {}} { + set committer_time $author_time + } + + set after_time [_format_offset_date $this $committer_time [expr {-$radius}]] + set before_time [_format_offset_date $this $committer_time $radius] + + lappend cmdline --after=$after_time --before=$before_time + } + + lappend cmdline $cmit + + set base_rev "HEAD" + if {$commit ne {}} { + set base_rev $commit + } + + if {$base_rev ne $cmit} { + lappend cmdline $base_rev + } + + do_gitk $cmdline + } +} + +method _blameparent {} { + global nullid + + set dat [_get_click_amov_info $this] + if {$dat ne {}} { + set cmit [lindex $dat 0] + set new_path [lindex $dat 1] + + # Allow using Blame Parent on lines modified in the working copy + if {$cmit eq $nullid} { + set parent_ref "HEAD" + } else { + set parent_ref "$cmit^" + } + if {[catch {set cparent [git rev-parse --verify $parent_ref]} err]} { + error_popup [strcat [mc "Cannot find parent commit:"] "\n\n$err"] + return; + } + + _kill $this + + # Generate a diff between the commit and its parent, + # and use the hunks to update the line number. + # Request zero context to simplify calculations. + if {$cmit eq $nullid} { + set diffcmd [list diff-index --unified=0 $cparent -- $new_path] + } else { + set diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path] + } + if {[catch {set fd [eval git_read $diffcmd]} err]} { + $status_operation stop [mc "Unable to display parent"] + error_popup [strcat [mc "Error loading diff:"] "\n\n$err"] + return + } + + set r_orig_line [lindex $dat 2] + + fconfigure $fd \ + -blocking 0 \ + -encoding binary \ + -translation binary + fileevent $fd readable [cb _read_diff_load_commit \ + $fd $cparent $new_path $r_orig_line] + set current_fd $fd + } +} + +method _read_diff_load_commit {fd cparent new_path tline} { + if {$fd ne $current_fd} { + catch {close $fd} + return + } + + while {[gets $fd line] >= 0} { + if {[regexp {^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@} $line line \ + old_line osz old_size new_line nsz new_size]} { + + if {$osz eq {}} { set old_size 1 } + if {$nsz eq {}} { set new_size 1 } + + if {$new_line <= $tline} { + if {[expr {$new_line + $new_size}] > $tline} { + # Target line within the hunk + set line_shift [expr { + ($new_size-$old_size)*($tline-$new_line)/$new_size + }] + } else { + set line_shift [expr {$new_size-$old_size}] + } + + set r_orig_line [expr {$r_orig_line - $line_shift}] + } + } + } + + if {[eof $fd]} { + close $fd + set current_fd {} + + _load_new_commit $this \ + $cparent \ + $new_path \ + [list $r_orig_line] + } +} ifdeleted { catch {close $fd} } + +method _show_tooltip {cur_w pos} { + if {$tooltip_wm ne {}} { + _open_tooltip $this $cur_w + } elseif {$tooltip_timer eq {}} { + set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]] + } +} + +method _open_tooltip {cur_w} { + set tooltip_timer {} + set pos_x [winfo pointerx $cur_w] + set pos_y [winfo pointery $cur_w] + if {[winfo containing $pos_x $pos_y] ne $cur_w} { + _hide_tooltip $this + return + } + + if {$tooltip_wm ne "$cur_w.tooltip"} { + _hide_tooltip $this + + set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1] + catch {wm attributes $tooltip_wm -type tooltip} + wm overrideredirect $tooltip_wm 1 + wm transient $tooltip_wm [winfo toplevel $cur_w] + set tooltip_t $tooltip_wm.label + text $tooltip_t \ + -takefocus 0 \ + -highlightthickness 0 \ + -relief flat \ + -borderwidth 0 \ + -wrap none \ + -background lightyellow \ + -foreground black + $tooltip_t tag conf section_header -font font_uibold + pack $tooltip_t + } else { + $tooltip_t conf -state normal + $tooltip_t delete 0.0 end + } + + set pos @[join [list \ + [expr {$pos_x - [winfo rootx $cur_w]}] \ + [expr {$pos_y - [winfo rooty $cur_w]}]] ,] + set lno [lindex [split [$cur_w index $pos] .] 0] + if {$cur_w eq $w_amov} { + set dat [lindex $amov_data $lno] + set org {} + } else { + set dat [lindex $asim_data $lno] + set org [lindex $amov_data $lno] + } + + if {$dat eq {}} { + _hide_tooltip $this + return + } + + set cmit [lindex $dat 0] + set tooltip_commit [list $cmit] + + set author_name {} + set summary {} + set author_time {} + catch {set author_name $header($cmit,author)} + catch {set summary $header($cmit,summary)} + catch {set author_time [format_date $header($cmit,author-time)]} + + $tooltip_t insert end "commit $cmit\n" + $tooltip_t insert end "$author_name $author_time\n" + $tooltip_t insert end "$summary" + + if {$org ne {} && [lindex $org 0] ne $cmit} { + set save [$tooltip_t get 0.0 end] + $tooltip_t delete 0.0 end + + set cmit [lindex $org 0] + set file [lindex $org 1] + lappend tooltip_commit $cmit + + set author_name {} + set summary {} + set author_time {} + catch {set author_name $header($cmit,author)} + catch {set summary $header($cmit,summary)} + catch {set author_time [format_date $header($cmit,author-time)]} + + $tooltip_t insert end [strcat [mc "Originally By:"] "\n"] section_header + $tooltip_t insert end "commit $cmit\n" + $tooltip_t insert end "$author_name $author_time\n" + $tooltip_t insert end "$summary\n" + + if {$file ne $path} { + $tooltip_t insert end [strcat [mc "In File:"] " "] section_header + $tooltip_t insert end "$file\n" + } + + $tooltip_t insert end "\n" + $tooltip_t insert end [strcat [mc "Copied Or Moved Here By:"] "\n"] section_header + $tooltip_t insert end $save + } + + $tooltip_t conf -state disabled + _position_tooltip $this + + # On MacOS raising a window causes it to acquire focus. + # Tk 8.5 on MacOS seems to properly support wm transient, + # so we can safely counter the effect there. + if {$::have_tk85 && [is_MacOSX]} { + update + if {$w eq {}} { + raise . + } else { + raise $w + } + } +} + +method _position_tooltip {} { + set max_h [lindex [split [$tooltip_t index end] .] 0] + set max_w 0 + for {set i 1} {$i <= $max_h} {incr i} { + set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1] + if {$c > $max_w} {set max_w $c} + } + $tooltip_t conf -width $max_w -height $max_h + + set req_w [winfo reqwidth $tooltip_t] + set req_h [winfo reqheight $tooltip_t] + set pos_x [expr {[winfo pointerx .] + 5}] + set pos_y [expr {[winfo pointery .] + 10}] + + set g "${req_w}x${req_h}" + if {[tk windowingsystem] eq "win32" || $pos_x >= 0} {append g +} + append g $pos_x + if {[tk windowingsystem] eq "win32" || $pos_y >= 0} {append g +} + append g $pos_y + + wm geometry $tooltip_wm $g + if {![is_MacOSX]} { + raise $tooltip_wm + } +} + +method _hide_tooltip {} { + if {$tooltip_wm ne {}} { + destroy $tooltip_wm + set tooltip_wm {} + set tooltip_commit {} + } + if {$tooltip_timer ne {}} { + after cancel $tooltip_timer + set tooltip_timer {} + } +} + +method _resize {new_height} { + set diff [expr {$new_height - $old_height}] + if {$diff == 0} return + + set my [expr {[winfo height $w.file_pane] - 25}] + set o [$w.file_pane sash coord 0] + set ox [lindex $o 0] + set oy [expr {[lindex $o 1] + $diff}] + if {$oy < 0} {set oy 0} + if {$oy > $my} {set oy $my} + $w.file_pane sash place 0 $ox $oy + + set old_height $new_height +} + +method _show_finder {} { + linebar::hide $gotoline + searchbar::show $finder +} + +method _show_linebar {} { + searchbar::hide $finder + linebar::show $gotoline +} + +} diff --git a/git-gui/lib/branch.tcl b/git-gui/lib/branch.tcl new file mode 100644 index 0000000..8b0c485 --- /dev/null +++ b/git-gui/lib/branch.tcl @@ -0,0 +1,40 @@ +# git-gui branch (create/delete) support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc load_all_heads {} { + global some_heads_tracking + + set rh refs/heads + set rh_len [expr {[string length $rh] + 1}] + set all_heads [list] + set fd [git_read for-each-ref --format=%(refname) $rh] + fconfigure $fd -translation binary -encoding utf-8 + while {[gets $fd line] > 0} { + if {!$some_heads_tracking || ![is_tracking_branch $line]} { + lappend all_heads [string range $line $rh_len end] + } + } + close $fd + + return [lsort $all_heads] +} + +proc load_all_tags {} { + set all_tags [list] + set fd [git_read for-each-ref \ + --sort=-taggerdate \ + --format=%(refname) \ + refs/tags] + fconfigure $fd -translation binary -encoding utf-8 + while {[gets $fd line] > 0} { + if {![regsub ^refs/tags/ $line {} name]} continue + lappend all_tags $name + } + close $fd + return $all_tags +} + +proc radio_selector {varname value args} { + upvar #0 $varname var + set var $value +} diff --git a/git-gui/lib/branch_checkout.tcl b/git-gui/lib/branch_checkout.tcl new file mode 100644 index 0000000..d06037d --- /dev/null +++ b/git-gui/lib/branch_checkout.tcl @@ -0,0 +1,93 @@ +# git-gui branch checkout support +# Copyright (C) 2007 Shawn Pearce + +class branch_checkout { + +field w ; # widget path +field w_rev ; # mega-widget to pick the initial revision + +field opt_fetch 1; # refetch tracking branch if used? +field opt_detach 0; # force a detached head case? + +constructor dialog {} { + global use_ttk NS + make_dialog top w + wm withdraw $w + wm title $top [mc "%s (%s): Checkout Branch" [appname] [reponame]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + } + + ${NS}::label $w.header -text [mc "Checkout Branch"] \ + -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.create -text [mc Checkout] \ + -default active \ + -command [cb _checkout] + pack $w.buttons.create -side right + ${NS}::button $w.buttons.cancel -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + set w_rev [::choose_rev::new $w.rev [mc Revision]] + $w_rev bind_listbox <Double-Button-1> [cb _checkout] + pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5 + + ${NS}::labelframe $w.options -text [mc Options] + + ${NS}::checkbutton $w.options.fetch \ + -text [mc "Fetch Tracking Branch"] \ + -variable @opt_fetch + pack $w.options.fetch -anchor nw + + ${NS}::checkbutton $w.options.detach \ + -text [mc "Detach From Local Branch"] \ + -variable @opt_detach + pack $w.options.detach -anchor nw + + pack $w.options -anchor nw -fill x -pady 5 -padx 5 + + bind $w <Visibility> [cb _visible] + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _checkout]\;break + wm deiconify $w + tkwait window $w +} + +method _checkout {} { + set spec [$w_rev get_tracking_branch] + if {$spec ne {} && $opt_fetch} { + set new {} + } elseif {[catch {set new [$w_rev commit_or_die]}]} { + return + } + + if {$opt_detach} { + set ref {} + } else { + set ref [$w_rev get_local_branch] + } + + set co [::checkout_op::new [$w_rev get] $new $ref] + $co parent $w + $co enable_checkout 1 + if {$spec ne {} && $opt_fetch} { + $co enable_fetch $spec + } + + if {[$co run]} { + destroy $w + } else { + $w_rev focus_filter + } +} + +method _visible {} { + grab $w + $w_rev focus_filter +} + +} diff --git a/git-gui/lib/branch_create.tcl b/git-gui/lib/branch_create.tcl new file mode 100644 index 0000000..ba367d5 --- /dev/null +++ b/git-gui/lib/branch_create.tcl @@ -0,0 +1,224 @@ +# git-gui branch create support +# Copyright (C) 2006, 2007 Shawn Pearce + +class branch_create { + +field w ; # widget path +field w_rev ; # mega-widget to pick the initial revision +field w_name ; # new branch name widget + +field name {}; # name of the branch the user has chosen +field name_type user; # type of branch name to use + +field opt_merge ff; # type of merge to apply to existing branch +field opt_checkout 1; # automatically checkout the new branch? +field opt_fetch 1; # refetch tracking branch if used? +field reset_ok 0; # did the user agree to reset? + +constructor dialog {} { + global repo_config use_ttk NS + + make_dialog top w + wm withdraw $w + wm title $top [mc "%s (%s): Create Branch" [appname] [reponame]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + } + + ${NS}::label $w.header -text [mc "Create New Branch"] \ + -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.create -text [mc Create] \ + -default active \ + -command [cb _create] + pack $w.buttons.create -side right + ${NS}::button $w.buttons.cancel -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::labelframe $w.desc -text [mc "Branch Name"] + ${NS}::radiobutton $w.desc.name_r \ + -text [mc "Name:"] \ + -value user \ + -variable @name_type + if {!$use_ttk} {$w.desc.name_r configure -anchor w} + set w_name $w.desc.name_t + ${NS}::entry $w_name \ + -width 40 \ + -textvariable @name \ + -validate key \ + -validatecommand [cb _validate %d %S] + grid $w.desc.name_r $w_name -sticky we -padx {0 5} + + ${NS}::radiobutton $w.desc.match_r \ + -text [mc "Match Tracking Branch Name"] \ + -value match \ + -variable @name_type + if {!$use_ttk} {$w.desc.match_r configure -anchor w} + grid $w.desc.match_r -sticky we -padx {0 5} -columnspan 2 + + grid columnconfigure $w.desc 1 -weight 1 + pack $w.desc -anchor nw -fill x -pady 5 -padx 5 + + set w_rev [::choose_rev::new $w.rev [mc "Starting Revision"]] + pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5 + + ${NS}::labelframe $w.options -text [mc Options] + + ${NS}::frame $w.options.merge + ${NS}::label $w.options.merge.l -text [mc "Update Existing Branch:"] + pack $w.options.merge.l -side left + ${NS}::radiobutton $w.options.merge.no \ + -text [mc No] \ + -value none \ + -variable @opt_merge + pack $w.options.merge.no -side left + ${NS}::radiobutton $w.options.merge.ff \ + -text [mc "Fast Forward Only"] \ + -value ff \ + -variable @opt_merge + pack $w.options.merge.ff -side left + ${NS}::radiobutton $w.options.merge.reset \ + -text [mc Reset] \ + -value reset \ + -variable @opt_merge + pack $w.options.merge.reset -side left + pack $w.options.merge -anchor nw + + ${NS}::checkbutton $w.options.fetch \ + -text [mc "Fetch Tracking Branch"] \ + -variable @opt_fetch + pack $w.options.fetch -anchor nw + + ${NS}::checkbutton $w.options.checkout \ + -text [mc "Checkout After Creation"] \ + -variable @opt_checkout + pack $w.options.checkout -anchor nw + pack $w.options -anchor nw -fill x -pady 5 -padx 5 + + trace add variable @name_type write [cb _select] + + set name $repo_config(gui.newbranchtemplate) + if {[is_config_true gui.matchtrackingbranch]} { + set name_type match + } + + bind $w <Visibility> [cb _visible] + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _create]\;break + wm deiconify $w + tkwait window $w +} + +method _create {} { + global repo_config + global M1B + + set spec [$w_rev get_tracking_branch] + switch -- $name_type { + user { + set newbranch $name + } + match { + if {$spec eq {}} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Please select a tracking branch."] + return + } + if {![regsub ^refs/heads/ [lindex $spec 2] {} newbranch]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Tracking branch %s is not a branch in the remote repository." [$w get]] + return + } + } + } + + if {$newbranch eq {} + || $newbranch eq $repo_config(gui.newbranchtemplate)} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Please supply a branch name."] + focus $w_name + return + } + + if {[catch {git check-ref-format "heads/$newbranch"}]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "'%s' is not an acceptable branch name." $newbranch] + focus $w_name + return + } + + if {$spec ne {} && $opt_fetch} { + set new {} + } elseif {[catch {set new [$w_rev commit_or_die]}]} { + return + } + + set co [::checkout_op::new \ + [$w_rev get] \ + $new \ + refs/heads/$newbranch] + $co parent $w + $co enable_create 1 + $co enable_merge $opt_merge + $co enable_checkout $opt_checkout + if {$spec ne {} && $opt_fetch} { + $co enable_fetch $spec + } + if {$spec ne {}} { + $co remote_source $spec + } + + if {[$co run]} { + destroy $w + } else { + focus $w_name + } +} + +method _validate {d S} { + if {$d == 1} { + if {[regexp {[~^:?*\[\0- ]} $S]} { + return 0 + } + if {[string length $S] > 0} { + set name_type user + } + } + return 1 +} + +method _select {args} { + if {$name_type eq {match}} { + $w_rev pick_tracking_branch + } +} + +method _visible {} { + grab $w + if {$name_type eq {user}} { + $w_name icursor end + focus $w_name + } +} + +} diff --git a/git-gui/lib/branch_delete.tcl b/git-gui/lib/branch_delete.tcl new file mode 100644 index 0000000..a505163 --- /dev/null +++ b/git-gui/lib/branch_delete.tcl @@ -0,0 +1,147 @@ +# git-gui branch delete support +# Copyright (C) 2007 Shawn Pearce + +class branch_delete { + +field w ; # widget path +field w_heads ; # listbox of local head names +field w_check ; # revision picker for merge test +field w_delete ; # delete button + +constructor dialog {} { + global current_branch use_ttk NS + + make_dialog top w + wm withdraw $w + wm title $top [mc "%s (%s): Delete Branch" [appname] [reponame]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + } + + ${NS}::label $w.header -text [mc "Delete Local Branch"] \ + -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + set w_delete $w.buttons.delete + ${NS}::button $w_delete \ + -text [mc Delete] \ + -default active \ + -state disabled \ + -command [cb _delete] + pack $w_delete -side right + ${NS}::button $w.buttons.cancel \ + -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::labelframe $w.list -text [mc "Local Branches"] + set w_heads $w.list.l + slistbox $w_heads \ + -height 10 \ + -width 70 \ + -selectmode extended \ + -exportselection false + pack $w.list.l -side left -fill both -expand 1 + pack $w.list -fill both -expand 1 -pady 5 -padx 5 + + set w_check [choose_rev::new \ + $w.check \ + [mc "Delete Only If Merged Into"] \ + ] + $w_check none [mc "Always (Do not perform merge checks)"] + pack $w.check -anchor nw -fill x -pady 5 -padx 5 + + foreach h [load_all_heads] { + if {$h ne $current_branch} { + $w_heads insert end $h + } + } + + bind $w_heads <<ListboxSelect>> [cb _select] + bind $w <Visibility> " + grab $w + focus $w + " + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _delete]\;break + wm deiconify $w + tkwait window $w +} + +method _select {} { + if {[$w_heads curselection] eq {}} { + $w_delete configure -state disabled + } else { + $w_delete configure -state normal + } +} + +method _delete {} { + if {[catch {set check_cmt [$w_check commit_or_die]}]} { + return + } + + set to_delete [list] + set not_merged [list] + foreach i [$w_heads curselection] { + set b [$w_heads get $i] + if {[catch { + set o [git rev-parse --verify "refs/heads/$b"] + }]} continue + if {$check_cmt ne {}} { + if {[catch {set m [git merge-base $o $check_cmt]}]} continue + if {$o ne $m} { + lappend not_merged $b + continue + } + } + lappend to_delete [list $b $o] + } + if {$not_merged ne {}} { + set msg "[mc "The following branches are not completely merged into %s:" [$w_check get]] + + - [join $not_merged "\n - "]" + tk_messageBox \ + -icon info \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message $msg + } + if {$to_delete eq {}} return + if {$check_cmt eq {}} { + set msg [mc "Recovering deleted branches is difficult.\n\nDelete the selected branches?"] + if {[tk_messageBox \ + -icon warning \ + -type yesno \ + -title [wm title $w] \ + -parent $w \ + -message $msg] ne yes} { + return + } + } + + set failed {} + foreach i $to_delete { + set b [lindex $i 0] + set o [lindex $i 1] + if {[catch {git branch -D $b} err]} { + append failed [mc " - %s:" $b] " $err\n" + } + } + + if {$failed ne {}} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Failed to delete branches:\n%s" $failed] + } + + destroy $w +} + +} diff --git a/git-gui/lib/branch_rename.tcl b/git-gui/lib/branch_rename.tcl new file mode 100644 index 0000000..3a2d79a --- /dev/null +++ b/git-gui/lib/branch_rename.tcl @@ -0,0 +1,134 @@ +# git-gui branch rename support +# Copyright (C) 2007 Shawn Pearce + +class branch_rename { + +field w +field oldname +field newname + +constructor dialog {} { + global current_branch use_ttk NS + + make_dialog top w + wm withdraw $w + wm title $top [mc "%s (%s): Rename Branch" [appname] [reponame]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + } + + set oldname $current_branch + set newname [get_config gui.newbranchtemplate] + + ${NS}::label $w.header -text [mc "Rename Branch"]\ + -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.rename -text [mc Rename] \ + -default active \ + -command [cb _rename] + pack $w.buttons.rename -side right + ${NS}::button $w.buttons.cancel -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::frame $w.rename + ${NS}::label $w.rename.oldname_l -text [mc "Branch:"] + if {$use_ttk} { + ttk::combobox $w.rename.oldname_m -textvariable @oldname \ + -values [load_all_heads] -state readonly + } else { + eval tk_optionMenu $w.rename.oldname_m @oldname [load_all_heads] + } + + ${NS}::label $w.rename.newname_l -text [mc "New Name:"] + ${NS}::entry $w.rename.newname_t \ + -width 40 \ + -textvariable @newname \ + -validate key \ + -validatecommand { + if {%d == 1 && [regexp {[~^:?*\[\0- ]} %S]} {return 0} + return 1 + } + + grid $w.rename.oldname_l $w.rename.oldname_m -sticky we -padx {0 5} + grid $w.rename.newname_l $w.rename.newname_t -sticky we -padx {0 5} + grid columnconfigure $w.rename 1 -weight 1 + pack $w.rename -anchor nw -fill x -pady 5 -padx 5 + + bind $w <Key-Return> [cb _rename] + bind $w <Key-Escape> [list destroy $w] + bind $w <Visibility> " + grab $w + $w.rename.newname_t icursor end + focus $w.rename.newname_t + " + wm deiconify $w + tkwait window $w +} + +method _rename {} { + global current_branch + + if {$oldname eq {}} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Please select a branch to rename."] + focus $w.rename.oldname_m + return + } + if {$newname eq {} + || $newname eq [get_config gui.newbranchtemplate]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Please supply a branch name."] + focus $w.rename.newname_t + return + } + if {![catch {git show-ref --verify -- "refs/heads/$newname"}]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Branch '%s' already exists." $newname] + focus $w.rename.newname_t + return + } + if {[catch {git check-ref-format "heads/$newname"}]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "'%s' is not an acceptable branch name." $newname] + focus $w.rename.newname_t + return + } + + if {[catch {git branch -m $oldname $newname} err]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [strcat [mc "Failed to rename '%s'." $oldname] "\n\n$err"] + return + } + + if {$current_branch eq $oldname} { + set current_branch $newname + } + + destroy $w +} + +} diff --git a/git-gui/lib/browser.tcl b/git-gui/lib/browser.tcl new file mode 100644 index 0000000..a982983 --- /dev/null +++ b/git-gui/lib/browser.tcl @@ -0,0 +1,322 @@ +# git-gui tree browser +# Copyright (C) 2006, 2007 Shawn Pearce + +class browser { + +image create photo ::browser::img_parent -data {R0lGODlhEAAQAIUAAPwCBBxSHBxOHMTSzNzu3KzCtBRGHCSKFIzCjLzSxBQ2FAxGHDzCLCyeHBQ+FHSmfAwuFBxKLDSCNMzizISyjJzOnDSyLAw+FAQSDAQeDBxWJAwmDAQOBKzWrDymNAQaDAQODAwaDDyKTFSyXFTGTEy6TAQCBAQKDAwiFBQyHAwSFAwmHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAZ1QIBwSCwaj0hiQCBICpcDQsFgGAaIguhhi0gohIsrQEDYMhiNrRfgeAQC5fMCAolIDhD2hFI5WC4YRBkaBxsOE2l/RxsHHA4dHmkfRyAbIQ4iIyQlB5NFGCAACiakpSZEJyinTgAcKSesACorgU4mJ6uxR35BACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=} +image create photo ::browser::img_rblob -data {R0lGODlhEAAQAIUAAPwCBFxaXNze3Ly2rJSWjPz+/Ozq7GxqbJyanPT29HRydMzOzDQyNIyKjERCROTi3Pz69PTy7Pzy7PTu5Ozm3LyqlJyWlJSSjJSOhOzi1LyulPz27PTq3PTm1OzezLyqjIyKhJSKfOzaxPz29OzizLyidIyGdIyCdOTOpLymhOzavOTStMTCtMS+rMS6pMSynMSulLyedAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAaQQIAQECgajcNkQMBkDgKEQFK4LFgLhkMBIVUKroWEYlEgMLxbBKLQUBwc52HgAQ4LBo049atWQyIPA3pEdFcQEhMUFYNVagQWFxgZGoxfYRsTHB0eH5UJCJAYICEinUoPIxIcHCQkIiIllQYEGCEhJicoKYwPmiQeKisrKLFKLCwtLi8wHyUlMYwM0tPUDH5BACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=} +image create photo ::browser::img_xblob -data {R0lGODlhEAAQAIYAAPwCBFRWVFxaXNza3OTi3Nze3Ly2tJyanPz+/Ozq7GxubNzSxMzOzMTGxHRybDQyNLy+vHRydHx6fKSipISChIyKjGxqbERCRCwuLLy6vGRiZExKTCQiJAwKDLSytLy2rJSSlHx+fDw6PKyqrBQWFPTu5Ozm3LyulLS2tCQmJAQCBPTq3Ozi1MSynCwqLAQGBOTazOzizOzezLyqjBweHNzSvOzaxKyurHRuZNzOtLymhDw+PIyCdOzWvOTOpLyidNzKtOTStLyifMTCtMS+rLyedAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAfZgACCAAEChYeGg4oCAwQFjgYBBwGKggEECJkICQoIkwADCwwNDY2mDA4Lng8QDhESsLARExQVDhYXGBkWExIaGw8cHR4SCQQfFQ8eFgUgIQEiwiMSBMYfGB4atwEXDyQd0wQlJicPKAHoFyIpJCoeDgMrLC0YKBsX6i4kL+4OMDEyZijr5oLGNxUqUCioEcPGDAwjPNyI6MEDChQjcOSwsUDHgw07RIgI4KCkAgs8cvTw8eOBogAxQtXIASTISiEuBwUYMoRIixYnZggpUgTDywdIkWJIitRPIAAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} +image create photo ::browser::img_tree -data {R0lGODlhEAAQAIYAAPwCBAQCBExKTBwWHMzKzOzq7ERCRExGTCwqLARqnAQ+ZHR2dKyqrNTOzHx2fCQiJMTi9NTu9HzC3AxmnAQ+XPTm7Dy67DymzITC3IzG5AxypHRydKymrMzOzOzu7BweHByy9AyGtFyy1IzG3NTu/ARupFRSVByazBR6rAyGvFyuzJTK3MTm9BR+tAxWhHS61MTi7Pz+/IymvCxulBRelAx2rHS63Pz6/PTy9PTu9Nza3ISitBRupFSixNTS1CxqnDQyNMzGzOTi5MTCxMTGxGxubGxqbLy2vLSutGRiZLy6vLSytKyurDQuNFxaXKSipDw6PAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAfDgACCAAECg4eIAAMEBQYHCImDBgkKCwwNBQIBBw4Bhw8QERITFJYEFQUFnoIPFhcYoRkaFBscHR4Ggh8gIRciEiMQJBkltCa6JyUoKSkXKhIrLCQYuQAPLS4TEyUhKb0qLzDVAjEFMjMuNBMoNcw21QY3ODkFOjs82RM1PfDzFRU3fOggcM7Fj2pAgggRokOHDx9DhhAZUqQaISBGhjwMEvEIkiIHEgUAkgSJkiNLmFSMJChAEydPGBSBwvJQgAc0/QQCACH+aENyZWF0ZWQgYnkgQk1QVG9HSUYgUHJvIHZlcnNpb24gMi41DQqpIERldmVsQ29yIDE5OTcsMTk5OC4gQWxsIHJpZ2h0cyByZXNlcnZlZC4NCmh0dHA6Ly93d3cuZGV2ZWxjb3IuY29tADs=} +image create photo ::browser::img_symlink -data {R0lGODlhEAAQAIQAAPwCBCwqLLSytLy+vERGRFRWVDQ2NKSmpAQCBKyurMTGxISChJyanHR2dIyKjGxubHRydGRmZIyOjFxeXHx6fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAVbICACwWieY1CibCCsrBkMb0zchSEcNYskCtqBBzshFkOGQFk0IRqOxqPBODRHCMhCQKteRc9FI/KQWGOIyFYgkDC+gPR4snCcfRGKOIKIgSMQE31+f4OEYCZ+IQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} +image create photo ::browser::img_unknown -data {R0lGODlhEAAQAIUAAPwCBFxaXIyKjNTW1Nze3LS2tJyanER2RGS+VPz+/PTu5GxqbPz69BQ6BCxeLFSqRPT29HRydMzOzDQyNERmPKSypCRWHIyKhERCRDyGPKz2nESiLBxGHCyCHGxubPz6/PTy7Ozi1Ly2rKSipOzm3LyqlKSWhCRyFOzizLymhNTKtNzOvOzaxOTStPz27OzWvOTOpLSupLyedMS+rMS6pMSulLyqjLymfLyifAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAamQIAQECgajcOkYEBoDgoBQyAJOCCuiENCsWBIh9aGw9F4HCARiXciRDQoBUnlYRlcIgsMG5CxXAgMGhscBRAEBRd7AB0eBBoIgxUfICEiikSPgyMMIAokJZcBkBybJgomIaBJAZoMpyCmqkMBFCcVCrgKKAwpoSorKqchKCwtvasIFBIhLiYvLzDHsxQNMcMKLDAwMqEz3jQ1NTY3ONyrE+jp6hN+QQAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} + +field w +field browser_commit +field browser_path +field browser_files {} +field browser_status [mc "Starting..."] +field browser_stack {} +field browser_busy 1 + +field ls_buf {}; # Buffered record output from ls-tree + +constructor new {commit {path {}}} { + global cursor_ptr M1B use_ttk NS + make_dialog top w + wm withdraw $top + wm title $top [mc "%s (%s): File Browser" [appname] [reponame]] + + if {$path ne {}} { + if {[string index $path end] ne {/}} { + append path / + } + } + + set browser_commit $commit + set browser_path "$browser_commit:[escape_path $path]" + + ${NS}::label $w.path \ + -textvariable @browser_path \ + -anchor w \ + -justify left \ + -font font_uibold + if {!$use_ttk} { $w.path configure -borderwidth 1 -relief sunken} + pack $w.path -anchor w -side top -fill x + + ${NS}::frame $w.list + set w_list $w.list.l + text $w_list -background white -foreground black \ + -borderwidth 0 \ + -cursor $cursor_ptr \ + -state disabled \ + -wrap none \ + -height 20 \ + -width 70 \ + -xscrollcommand [list $w.list.sbx set] \ + -yscrollcommand [list $w.list.sby set] + rmsel_tag $w_list + ${NS}::scrollbar $w.list.sbx -orient h -command [list $w_list xview] + ${NS}::scrollbar $w.list.sby -orient v -command [list $w_list yview] + pack $w.list.sbx -side bottom -fill x + pack $w.list.sby -side right -fill y + pack $w_list -side left -fill both -expand 1 + pack $w.list -side top -fill both -expand 1 + + ${NS}::label $w.status \ + -textvariable @browser_status \ + -anchor w \ + -justify left + if {!$use_ttk} { $w.status configure -borderwidth 1 -relief sunken} + pack $w.status -anchor w -side bottom -fill x + + bind $w_list <Button-1> "[cb _click 0 @%x,%y];break" + bind $w_list <Double-Button-1> "[cb _click 1 @%x,%y];break" + bind $w_list <$M1B-Up> "[cb _parent] ;break" + bind $w_list <$M1B-Left> "[cb _parent] ;break" + bind $w_list <Up> "[cb _move -1] ;break" + bind $w_list <Down> "[cb _move 1] ;break" + bind $w_list <$M1B-Right> "[cb _enter] ;break" + bind $w_list <Return> "[cb _enter] ;break" + bind $w_list <Prior> "[cb _page -1] ;break" + bind $w_list <Next> "[cb _page 1] ;break" + bind $w_list <Left> break + bind $w_list <Right> break + + bind $w_list <Visibility> [list focus $w_list] + wm deiconify $top + set w $w_list + if {$path ne {}} { + _ls $this $browser_commit:$path $path + } else { + _ls $this $browser_commit $path + } + return $this +} + +method _move {dir} { + if {$browser_busy} return + set lno [lindex [split [$w index in_sel.first] .] 0] + incr lno $dir + if {[lindex $browser_files [expr {$lno - 1}]] ne {}} { + $w tag remove in_sel 0.0 end + $w tag add in_sel $lno.0 [expr {$lno + 1}].0 + $w see $lno.0 + } +} + +method _page {dir} { + if {$browser_busy} return + $w yview scroll $dir pages + set lno [expr {int( + [lindex [$w yview] 0] + * [llength $browser_files] + + 1)}] + if {[lindex $browser_files [expr {$lno - 1}]] ne {}} { + $w tag remove in_sel 0.0 end + $w tag add in_sel $lno.0 [expr {$lno + 1}].0 + $w see $lno.0 + } +} + +method _parent {} { + if {$browser_busy} return + set info [lindex $browser_files 0] + if {[lindex $info 0] eq {parent}} { + set parent [lindex $browser_stack end-1] + set browser_stack [lrange $browser_stack 0 end-2] + if {$browser_stack eq {}} { + regsub {:.*$} $browser_path {:} browser_path + } else { + regsub {/[^/]+/$} $browser_path {/} browser_path + } + set browser_status [mc "Loading %s..." $browser_path] + _ls $this [lindex $parent 0] [lindex $parent 1] + } +} + +method _enter {} { + if {$browser_busy} return + set lno [lindex [split [$w index in_sel.first] .] 0] + set info [lindex $browser_files [expr {$lno - 1}]] + if {$info ne {}} { + switch -- [lindex $info 0] { + parent { + _parent $this + } + tree { + set name [lindex $info 2] + set escn [escape_path $name] + set browser_status [mc "Loading %s..." $escn] + append browser_path $escn + _ls $this [lindex $info 1] $name + } + blob { + set name [lindex $info 2] + set p {} + foreach n $browser_stack { + append p [lindex $n 1] + } + append p $name + blame::new $browser_commit $p {} + } + } + } +} + +method _click {was_double_click pos} { + if {$browser_busy} return + set lno [lindex [split [$w index $pos] .] 0] + focus $w + + if {[lindex $browser_files [expr {$lno - 1}]] ne {}} { + $w tag remove in_sel 0.0 end + $w tag add in_sel $lno.0 [expr {$lno + 1}].0 + if {$was_double_click} { + _enter $this + } + } +} + +method _ls {tree_id {name {}}} { + set ls_buf {} + set browser_files {} + set browser_busy 1 + + $w conf -state normal + $w tag remove in_sel 0.0 end + $w delete 0.0 end + if {$browser_stack ne {}} { + $w image create end \ + -align center -padx 5 -pady 1 \ + -name icon0 \ + -image ::browser::img_parent + $w insert end [mc "\[Up To Parent\]"] + lappend browser_files parent + } + lappend browser_stack [list $tree_id $name] + $w conf -state disabled + + set fd [git_read ls-tree -z $tree_id] + fconfigure $fd -blocking 0 -translation binary -encoding utf-8 + fileevent $fd readable [cb _read $fd] +} + +method _read {fd} { + append ls_buf [read $fd] + set pck [split $ls_buf "\0"] + set ls_buf [lindex $pck end] + + set n [llength $browser_files] + $w conf -state normal + foreach p [lrange $pck 0 end-1] { + set tab [string first "\t" $p] + if {$tab == -1} continue + + set info [split [string range $p 0 [expr {$tab - 1}]] { }] + set path [string range $p [expr {$tab + 1}] end] + set type [lindex $info 1] + set object [lindex $info 2] + + switch -- $type { + blob { + scan [lindex $info 0] %o mode + if {$mode == 0120000} { + set image ::browser::img_symlink + } elseif {($mode & 0100) != 0} { + set image ::browser::img_xblob + } else { + set image ::browser::img_rblob + } + } + tree { + set image ::browser::img_tree + append path / + } + default { + set image ::browser::img_unknown + } + } + + if {$n > 0} {$w insert end "\n"} + $w image create end \ + -align center -padx 5 -pady 1 \ + -name icon[incr n] \ + -image $image + $w insert end [escape_path $path] + lappend browser_files [list $type $object $path] + } + $w conf -state disabled + + if {[eof $fd]} { + close $fd + set browser_status [mc "Ready."] + set browser_busy 0 + set ls_buf {} + if {$n > 0} { + $w tag add in_sel 1.0 2.0 + focus -force $w + } + } +} ifdeleted { + catch {close $fd} +} + +} + +class browser_open { + +field w ; # widget path +field w_rev ; # mega-widget to pick the initial revision + +constructor dialog {} { + global use_ttk NS + make_dialog top w + wm withdraw $top + wm title $top [mc "%s (%s): Browse Branch Files" [appname] [reponame]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + wm transient $top . + } + + ${NS}::label $w.header \ + -text [mc "Browse Branch Files"] \ + -font font_uibold \ + -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.browse -text [mc Browse] \ + -default active \ + -command [cb _open] + pack $w.buttons.browse -side right + ${NS}::button $w.buttons.cancel -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + set w_rev [::choose_rev::new $w.rev [mc Revision]] + $w_rev bind_listbox <Double-Button-1> [cb _open] + pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5 + + bind $w <Visibility> [cb _visible] + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _open]\;break + wm deiconify $top + tkwait window $w +} + +method _open {} { + if {[catch {$w_rev commit_or_die} err]} { + return + } + set name [$w_rev get] + destroy $w + browser::new $name +} + +method _visible {} { + grab $w + $w_rev focus_filter +} + +} diff --git a/git-gui/lib/checkout_op.tcl b/git-gui/lib/checkout_op.tcl new file mode 100644 index 0000000..21ea768 --- /dev/null +++ b/git-gui/lib/checkout_op.tcl @@ -0,0 +1,646 @@ +# git-gui commit checkout support +# Copyright (C) 2007 Shawn Pearce + +class checkout_op { + +field w {}; # our window (if we have one) +field w_cons {}; # embedded console window object + +field new_expr ; # expression the user saw/thinks this is +field new_hash ; # commit SHA-1 we are switching to +field new_ref ; # ref we are updating/creating +field old_hash ; # commit SHA-1 that was checked out when we started + +field parent_w .; # window that started us +field merge_type none; # type of merge to apply to existing branch +field merge_base {}; # merge base if we have another ref involved +field fetch_spec {}; # refetch tracking branch if used? +field checkout 1; # actually checkout the branch? +field create 0; # create the branch if it doesn't exist? +field remote_source {}; # same as fetch_spec, to setup tracking + +field reset_ok 0; # did the user agree to reset? +field fetch_ok 0; # did the fetch succeed? + +field readtree_d {}; # buffered output from read-tree +field update_old {}; # was the update-ref call deferred? +field reflog_msg {}; # log message for the update-ref call + +constructor new {expr hash {ref {}}} { + set new_expr $expr + set new_hash $hash + set new_ref $ref + + return $this +} + +method parent {path} { + set parent_w [winfo toplevel $path] +} + +method enable_merge {type} { + set merge_type $type +} + +method enable_fetch {spec} { + set fetch_spec $spec +} + +method remote_source {spec} { + set remote_source $spec +} + +method enable_checkout {co} { + set checkout $co +} + +method enable_create {co} { + set create $co +} + +method run {} { + if {$fetch_spec ne {}} { + global M1B + + # We were asked to refresh a single tracking branch + # before we get to work. We should do that before we + # consider any ref updating. + # + set fetch_ok 0 + set l_trck [lindex $fetch_spec 0] + set remote [lindex $fetch_spec 1] + set r_head [lindex $fetch_spec 2] + regsub ^refs/heads/ $r_head {} r_name + + set cmd [list git fetch $remote] + if {$l_trck ne {}} { + lappend cmd +$r_head:$l_trck + } else { + lappend cmd $r_head + } + + _toplevel $this {Refreshing Tracking Branch} + set w_cons [::console::embed \ + $w.console \ + [mc "Fetching %s from %s" $r_name $remote]] + pack $w.console -fill both -expand 1 + $w_cons exec $cmd [cb _finish_fetch] + + bind $w <$M1B-Key-w> break + bind $w <$M1B-Key-W> break + bind $w <Visibility> " + [list grab $w] + [list focus $w] + " + wm protocol $w WM_DELETE_WINDOW [cb _noop] + tkwait window $w + + if {!$fetch_ok} { + delete_this + return 0 + } + } + + if {$new_ref ne {}} { + # If we have a ref we need to update it before we can + # proceed with a checkout (if one was enabled). + # + if {![_update_ref $this]} { + delete_this + return 0 + } + } + + if {$checkout} { + _checkout $this + return 1 + } + + delete_this + return 1 +} + +method _noop {} {} + +method _finish_fetch {ok} { + if {$ok} { + set l_trck [lindex $fetch_spec 0] + if {$l_trck eq {}} { + set l_trck FETCH_HEAD + } + if {[catch {set new_hash [git rev-parse --verify "$l_trck^0"]} err]} { + set ok 0 + $w_cons insert [mc "fatal: Cannot resolve %s" $l_trck] + $w_cons insert $err + } + } + + $w_cons done $ok + set w_cons {} + wm protocol $w WM_DELETE_WINDOW {} + + if {$ok} { + destroy $w + set w {} + } else { + button $w.close -text [mc Close] -command [list destroy $w] + pack $w.close -side bottom -anchor e -padx 10 -pady 10 + } + + set fetch_ok $ok +} + +method _update_ref {} { + global null_sha1 current_branch repo_config + + set ref $new_ref + set new $new_hash + + set is_current 0 + set rh refs/heads/ + set rn [string length $rh] + if {[string equal -length $rn $rh $ref]} { + set newbranch [string range $ref $rn end] + if {$current_branch eq $newbranch} { + set is_current 1 + } + } else { + set newbranch $ref + } + + if {[catch {set cur [git rev-parse --verify "$ref^0"]}]} { + # Assume it does not exist, and that is what the error was. + # + if {!$create} { + _error $this [mc "Branch '%s' does not exist." $newbranch] + return 0 + } + + set reflog_msg "branch: Created from $new_expr" + set cur $null_sha1 + + if {($repo_config(branch.autosetupmerge) eq {true} + || $repo_config(branch.autosetupmerge) eq {always}) + && $remote_source ne {} + && "refs/heads/$newbranch" eq $ref} { + + set c_remote [lindex $remote_source 1] + set c_merge [lindex $remote_source 2] + if {[catch { + git config branch.$newbranch.remote $c_remote + git config branch.$newbranch.merge $c_merge + } err]} { + _error $this [strcat \ + [mc "Failed to configure simplified git-pull for '%s'." $newbranch] \ + "\n\n$err"] + } + } + } elseif {$create && $merge_type eq {none}} { + # We were told to create it, but not do a merge. + # Bad. Name shouldn't have existed. + # + _error $this [mc "Branch '%s' already exists." $newbranch] + return 0 + } elseif {!$create && $merge_type eq {none}} { + # We aren't creating, it exists and we don't merge. + # We are probably just a simple branch switch. + # Use whatever value we just read. + # + set new $cur + set new_hash $cur + } elseif {$new eq $cur} { + # No merge would be required, don't compute anything. + # + } else { + catch {set merge_base [git merge-base $new $cur]} + if {$merge_base eq $cur} { + # The current branch is older. + # + set reflog_msg "merge $new_expr: Fast-forward" + } else { + switch -- $merge_type { + ff { + if {$merge_base eq $new} { + # The current branch is actually newer. + # + set new $cur + set new_hash $cur + } else { + _error $this [mc "Branch '%s' already exists.\n\nIt cannot fast-forward to %s.\nA merge is required." $newbranch $new_expr] + return 0 + } + } + reset { + # The current branch will lose things. + # + if {[_confirm_reset $this $cur]} { + set reflog_msg "reset $new_expr" + } else { + return 0 + } + } + default { + _error $this [mc "Merge strategy '%s' not supported." $merge_type] + return 0 + } + } + } + } + + if {$new ne $cur} { + if {$is_current} { + # No so fast. We should defer this in case + # we cannot update the working directory. + # + set update_old $cur + return 1 + } + + if {[catch { + git update-ref -m $reflog_msg $ref $new $cur + } err]} { + _error $this [strcat [mc "Failed to update '%s'." $newbranch] "\n\n$err"] + return 0 + } + } + + return 1 +} + +method _checkout {} { + if {[lock_index checkout_op]} { + after idle [cb _start_checkout] + } else { + _error $this [mc "Staging area (index) is already locked."] + delete_this + } +} + +method _start_checkout {} { + global HEAD commit_type + + # -- Our in memory state should match the repository. + # + repository_state curType old_hash curMERGE_HEAD + if {[string match amend* $commit_type] + && $curType eq {normal} + && $old_hash eq $HEAD} { + } elseif {$commit_type ne $curType || $HEAD ne $old_hash} { + info_popup [mc "Last scanned state does not match repository state. + +Another Git program has modified this repository since the last scan. A rescan must be performed before the current branch can be changed. + +The rescan will be automatically started now. +"] + unlock_index + rescan ui_ready + delete_this + return + } + + if {$old_hash eq $new_hash} { + _after_readtree $this + } elseif {[is_config_true gui.trustmtime]} { + _readtree $this + } else { + ui_status [mc "Refreshing file status..."] + set fd [git_read update-index \ + -q \ + --unmerged \ + --ignore-missing \ + --refresh \ + ] + fconfigure $fd -blocking 0 -translation binary + fileevent $fd readable [cb _refresh_wait $fd] + } +} + +method _refresh_wait {fd} { + read $fd + if {[eof $fd]} { + close $fd + _readtree $this + } +} + +method _name {} { + if {$new_ref eq {}} { + return [string range $new_hash 0 7] + } + + set rh refs/heads/ + set rn [string length $rh] + if {[string equal -length $rn $rh $new_ref]} { + return [string range $new_ref $rn end] + } else { + return $new_ref + } +} + +method _readtree {} { + global HEAD + + set readtree_d {} + set status_bar_operation [$::main_status start \ + [mc "Updating working directory to '%s'..." [_name $this]] \ + [mc "files checked out"]] + + set fd [git_read --stderr read-tree \ + -m \ + -u \ + -v \ + --exclude-per-directory=.gitignore \ + $HEAD \ + $new_hash \ + ] + fconfigure $fd -blocking 0 -translation binary + fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation] +} + +method _readtree_wait {fd status_bar_operation} { + global current_branch + + set buf [read $fd] + $status_bar_operation update_meter $buf + append readtree_d $buf + + fconfigure $fd -blocking 1 + if {![eof $fd]} { + fconfigure $fd -blocking 0 + $status_bar_operation stop + return + } + + if {[catch {close $fd}]} { + set err $readtree_d + regsub {^fatal: } $err {} err + $status_bar_operation stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]] + warn_popup [strcat [mc "File level merge required."] " + +$err + +" [mc "Staying on branch '%s'." $current_branch]] + unlock_index + delete_this + return + } + + $status_bar_operation stop + _after_readtree $this +} + +method _after_readtree {} { + global commit_type HEAD MERGE_HEAD PARENT + global current_branch is_detached + global ui_comm + + set name [_name $this] + set log "checkout: moving" + if {!$is_detached} { + append log " from $current_branch" + } + + # -- Move/create HEAD as a symbolic ref. Core git does not + # even check for failure here, it Just Works(tm). If it + # doesn't we are in some really ugly state that is difficult + # to recover from within git-gui. + # + set rh refs/heads/ + set rn [string length $rh] + if {[string equal -length $rn $rh $new_ref]} { + set new_branch [string range $new_ref $rn end] + if {$is_detached || $current_branch ne $new_branch} { + append log " to $new_branch" + if {[catch { + git symbolic-ref -m $log HEAD $new_ref + } err]} { + _fatal $this $err + } + set current_branch $new_branch + set is_detached 0 + } + } else { + if {!$is_detached || $new_hash ne $HEAD} { + append log " to $new_expr" + if {[catch { + _detach_HEAD $log $new_hash + } err]} { + _fatal $this $err + } + } + set current_branch HEAD + set is_detached 1 + } + + # -- We had to defer updating the branch itself until we + # knew the working directory would update. So now we + # need to finish that work. If it fails we're in big + # trouble. + # + if {$update_old ne {}} { + if {[catch { + git update-ref \ + -m $reflog_msg \ + $new_ref \ + $new_hash \ + $update_old + } err]} { + _fatal $this $err + } + } + + if {$is_detached} { + info_popup [mc "You are no longer on a local branch. + +If you wanted to be on a branch, create one now starting from 'This Detached Checkout'."] + } + + # -- Run the post-checkout hook. + # + set fd_ph [githook_read post-checkout $old_hash $new_hash 1] + if {$fd_ph ne {}} { + global pch_error + set pch_error {} + fconfigure $fd_ph -blocking 0 -translation binary -eofchar {} + fileevent $fd_ph readable [cb _postcheckout_wait $fd_ph] + } else { + _update_repo_state $this + } +} + +method _postcheckout_wait {fd_ph} { + global pch_error + + append pch_error [read $fd_ph] + fconfigure $fd_ph -blocking 1 + if {[eof $fd_ph]} { + if {[catch {close $fd_ph}]} { + hook_failed_popup post-checkout $pch_error 0 + } + unset pch_error + _update_repo_state $this + return + } + fconfigure $fd_ph -blocking 0 +} + +method _update_repo_state {} { + # -- Update our repository state. If we were previously in + # amend mode we need to toss the current buffer and do a + # full rescan to update our file lists. If we weren't in + # amend mode our file lists are accurate and we can avoid + # the rescan. + # + global commit_type_is_amend commit_type HEAD MERGE_HEAD PARENT + global ui_comm + + unlock_index + set name [_name $this] + set commit_type_is_amend 0 + if {[string match amend* $commit_type]} { + $ui_comm delete 0.0 end + $ui_comm edit reset + $ui_comm edit modified false + rescan [list ui_status [mc "Checked out '%s'." $name]] + } else { + repository_state commit_type HEAD MERGE_HEAD + set PARENT $HEAD + ui_status [mc "Checked out '%s'." $name] + } + delete_this +} + +git-version proc _detach_HEAD {log new} { + >= 1.5.3 { + git update-ref --no-deref -m $log HEAD $new + } + default { + set p [gitdir HEAD] + file delete $p + set fd [open $p w] + fconfigure $fd -translation lf -encoding utf-8 + puts $fd $new + close $fd + } +} + +method _confirm_reset {cur} { + set reset_ok 0 + set name [_name $this] + set gitk [list do_gitk [list $cur ^$new_hash]] + + _toplevel $this {Confirm Branch Reset} + pack [label $w.msg1 \ + -anchor w \ + -justify left \ + -text [mc "Resetting '%s' to '%s' will lose the following commits:" $name $new_expr]\ + ] -anchor w + + set list $w.list.l + frame $w.list + text $list \ + -font font_diff \ + -width 80 \ + -height 10 \ + -wrap none \ + -xscrollcommand [list $w.list.sbx set] \ + -yscrollcommand [list $w.list.sby set] + scrollbar $w.list.sbx -orient h -command [list $list xview] + scrollbar $w.list.sby -orient v -command [list $list yview] + pack $w.list.sbx -fill x -side bottom + pack $w.list.sby -fill y -side right + pack $list -fill both -expand 1 + pack $w.list -fill both -expand 1 -padx 5 -pady 5 + + pack [label $w.msg2 \ + -anchor w \ + -justify left \ + -text [mc "Recovering lost commits may not be easy."] \ + ] + pack [label $w.msg3 \ + -anchor w \ + -justify left \ + -text [mc "Reset '%s'?" $name] \ + ] + + frame $w.buttons + button $w.buttons.visualize \ + -text [mc Visualize] \ + -command $gitk + pack $w.buttons.visualize -side left + button $w.buttons.reset \ + -text [mc Reset] \ + -command " + set @reset_ok 1 + destroy $w + " + pack $w.buttons.reset -side right + button $w.buttons.cancel \ + -default active \ + -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + set fd [git_read rev-list --pretty=oneline $cur ^$new_hash] + while {[gets $fd line] > 0} { + set abbr [string range $line 0 7] + set subj [string range $line 41 end] + $list insert end "$abbr $subj\n" + } + close $fd + $list configure -state disabled + + bind $w <Key-v> $gitk + bind $w <Visibility> " + grab $w + focus $w.buttons.cancel + " + bind $w <Key-Return> [list destroy $w] + bind $w <Key-Escape> [list destroy $w] + tkwait window $w + return $reset_ok +} + +method _error {msg} { + if {[winfo ismapped $parent_w]} { + set p $parent_w + } else { + set p . + } + + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $p] \ + -parent $p \ + -message $msg +} + +method _toplevel {title} { + regsub -all {::} $this {__} w + set w .$w + + if {[winfo ismapped $parent_w]} { + set p $parent_w + } else { + set p . + } + + toplevel $w + wm title $w $title + wm geometry $w "+[winfo rootx $p]+[winfo rooty $p]" +} + +method _fatal {err} { + error_popup [strcat [mc "Failed to set current branch. + +This working directory is only partially switched. We successfully updated your files, but failed to update an internal Git file. + +This should not have occurred. %s will now close and give up." [appname]] " + +$err"] + exit 1 +} + +} diff --git a/git-gui/lib/choose_font.tcl b/git-gui/lib/choose_font.tcl new file mode 100644 index 0000000..ebe50bd --- /dev/null +++ b/git-gui/lib/choose_font.tcl @@ -0,0 +1,171 @@ +# git-gui font chooser +# Copyright (C) 2007 Shawn Pearce + +class choose_font { + +field w +field w_family ; # UI widget of all known family names +field w_example ; # Example to showcase the chosen font + +field f_family ; # Currently chosen family name +field f_size ; # Currently chosen point size + +field v_family ; # Name of global variable for family +field v_size ; # Name of global variable for size + +variable all_families [list] ; # All fonts known to Tk + +constructor pick {path title a_family a_size} { + variable all_families + global use_ttk NS + + set v_family $a_family + set v_size $a_size + + upvar #0 $v_family pv_family + upvar #0 $v_size pv_size + + set f_family $pv_family + set f_size $pv_size + + make_dialog top w + wm withdraw $top + wm title $top "[appname] ([reponame]): $title" + wm geometry $top "+[winfo rootx $path]+[winfo rooty $path]" + + ${NS}::label $w.header -text $title -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.select \ + -text [mc Select] \ + -default active \ + -command [cb _select] + ${NS}::button $w.buttons.cancel \ + -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.select -side right + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::frame $w.inner + + ${NS}::frame $w.inner.family + ${NS}::label $w.inner.family.l \ + -text [mc "Font Family"] \ + -anchor w + set w_family $w.inner.family.v + text $w_family \ + -background white \ + -foreground black \ + -borderwidth 1 \ + -relief sunken \ + -cursor $::cursor_ptr \ + -wrap none \ + -width 30 \ + -height 10 \ + -yscrollcommand [list $w.inner.family.sby set] + rmsel_tag $w_family + ${NS}::scrollbar $w.inner.family.sby -command [list $w_family yview] + pack $w.inner.family.l -side top -fill x + pack $w.inner.family.sby -side right -fill y + pack $w_family -fill both -expand 1 + + ${NS}::frame $w.inner.size + ${NS}::label $w.inner.size.l \ + -text [mc "Font Size"] \ + -anchor w + tspinbox $w.inner.size.v \ + -textvariable @f_size \ + -from 2 -to 80 -increment 1 \ + -width 3 + bind $w.inner.size.v <FocusIn> {%W selection range 0 end} + pack $w.inner.size.l -fill x -side top + pack $w.inner.size.v -fill x -padx 2 + + grid configure $w.inner.family $w.inner.size -sticky nsew + grid rowconfigure $w.inner 0 -weight 1 + grid columnconfigure $w.inner 0 -weight 1 + pack $w.inner -fill both -expand 1 -padx 5 -pady 5 + + ${NS}::frame $w.example + ${NS}::label $w.example.l \ + -text [mc "Font Example"] \ + -anchor w + set w_example $w.example.t + text $w_example \ + -background white \ + -foreground black \ + -borderwidth 1 \ + -relief sunken \ + -height 3 \ + -width 40 + rmsel_tag $w_example + $w_example tag conf example -justify center + $w_example insert end [mc "This is example text.\nIf you like this text, it can be your font."] example + $w_example conf -state disabled + pack $w.example.l -fill x + pack $w_example -fill x + pack $w.example -fill x -padx 5 + + if {$all_families eq {}} { + set all_families [lsort [font families]] + } + + $w_family tag conf pick + $w_family tag bind pick <Button-1> [cb _pick_family %x %y]\;break + foreach f $all_families { + set sel [list pick] + if {$f eq $f_family} { + lappend sel in_sel + } + $w_family insert end "$f\n" $sel + } + $w_family conf -state disabled + _update $this + + trace add variable @f_size write [cb _update] + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _select]\;break + bind $w <Visibility> " + grab $w + focus $w + " + wm deiconify $w + tkwait window $w +} + +method _select {} { + upvar #0 $v_family pv_family + upvar #0 $v_size pv_size + + set pv_family $f_family + set pv_size $f_size + + destroy $w +} + +method _pick_family {x y} { + variable all_families + + set i [lindex [split [$w_family index @$x,$y] .] 0] + set n [lindex $all_families [expr {$i - 1}]] + if {$n ne {}} { + $w_family tag remove in_sel 0.0 end + $w_family tag add in_sel $i.0 [expr {$i + 1}].0 + set f_family $n + _update $this + } +} + +method _update {args} { + variable all_families + + set i [lsearch -exact $all_families $f_family] + if {$i < 0} return + + $w_example tag conf example -font [list $f_family $f_size] + $w_family see [expr {$i + 1}].0 +} + +} diff --git a/git-gui/lib/choose_repository.tcl b/git-gui/lib/choose_repository.tcl new file mode 100644 index 0000000..e54f3e6 --- /dev/null +++ b/git-gui/lib/choose_repository.tcl @@ -0,0 +1,1170 @@ +# git-gui Git repository chooser +# Copyright (C) 2007 Shawn Pearce + +class choose_repository { + +field top +field w +field w_body ; # Widget holding the center content +field w_next ; # Next button +field w_quit ; # Quit button +field o_cons ; # Console object (if active) + +# Status mega-widget instance during _do_clone2 (used by _copy_files and +# _link_files). Widget is destroyed before _do_clone2 calls +# _do_clone_checkout +field o_status + +# Operation displayed by status mega-widget during _do_clone_checkout => +# _readtree_wait => _postcheckout_wait => _do_clone_submodules => +# _do_validate_submodule_cloning. The status mega-widget is a different +# instance than that stored in $o_status in earlier operations. +field o_status_op + +field w_types ; # List of type buttons in clone +field w_recentlist ; # Listbox containing recent repositories +field w_localpath ; # Entry widget bound to local_path + +field done 0 ; # Finished picking the repository? +field local_path {} ; # Where this repository is locally +field origin_url {} ; # Where we are cloning from +field origin_name origin ; # What we shall call 'origin' +field clone_type hardlink ; # Type of clone to construct +field recursive true ; # Recursive cloning flag +field readtree_err ; # Error output from read-tree (if any) +field sorted_recent ; # recent repositories (sorted) + +constructor pick {} { + global M1T M1B use_ttk NS + + if {[set maxrecent [get_config gui.maxrecentrepo]] eq {}} { + set maxrecent 10 + } + + make_dialog top w + wm title $top [mc "Git Gui"] + + if {$top eq {.}} { + menu $w.mbar -tearoff 0 + $top configure -menu $w.mbar + + set m_repo $w.mbar.repository + $w.mbar add cascade \ + -label [mc Repository] \ + -menu $m_repo + menu $m_repo + + if {[is_MacOSX]} { + $w.mbar add cascade -label Apple -menu .mbar.apple + menu $w.mbar.apple + $w.mbar.apple add command \ + -label [mc "About %s" [appname]] \ + -command do_about + $w.mbar.apple add command \ + -label [mc "Show SSH Key"] \ + -command do_ssh_key + } else { + $w.mbar add cascade -label [mc Help] -menu $w.mbar.help + menu $w.mbar.help + $w.mbar.help add command \ + -label [mc "About %s" [appname]] \ + -command do_about + $w.mbar.help add command \ + -label [mc "Show SSH Key"] \ + -command do_ssh_key + } + + wm protocol $top WM_DELETE_WINDOW exit + bind $top <$M1B-q> exit + bind $top <$M1B-Q> exit + bind $top <Key-Escape> exit + } else { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + bind $top <Key-Escape> [list destroy $top] + set m_repo {} + } + + pack [git_logo $w.git_logo] -side left -fill y -padx 10 -pady 10 + + set w_body $w.body + set opts $w_body.options + ${NS}::frame $w_body + text $opts \ + -cursor $::cursor_ptr \ + -relief flat \ + -background [get_bg_color $w_body] \ + -wrap none \ + -spacing1 5 \ + -width 50 \ + -height 3 + pack $opts -anchor w -fill x + + $opts tag conf link_new -foreground blue -underline 1 + $opts tag bind link_new <1> [cb _next new] + $opts insert end [mc "Create New Repository"] link_new + $opts insert end "\n" + if {$m_repo ne {}} { + $m_repo add command \ + -command [cb _next new] \ + -accelerator $M1T-N \ + -label [mc "New..."] + bind $top <$M1B-n> [cb _next new] + bind $top <$M1B-N> [cb _next new] + } + + $opts tag conf link_clone -foreground blue -underline 1 + $opts tag bind link_clone <1> [cb _next clone] + $opts insert end [mc "Clone Existing Repository"] link_clone + $opts insert end "\n" + if {$m_repo ne {}} { + if {[tk windowingsystem] eq "win32"} { + set key L + } else { + set key C + } + $m_repo add command \ + -command [cb _next clone] \ + -accelerator $M1T-$key \ + -label [mc "Clone..."] + bind $top <$M1B-[string tolower $key]> [cb _next clone] + bind $top <$M1B-[string toupper $key]> [cb _next clone] + } + + $opts tag conf link_open -foreground blue -underline 1 + $opts tag bind link_open <1> [cb _next open] + $opts insert end [mc "Open Existing Repository"] link_open + $opts insert end "\n" + if {$m_repo ne {}} { + $m_repo add command \ + -command [cb _next open] \ + -accelerator $M1T-O \ + -label [mc "Open..."] + bind $top <$M1B-o> [cb _next open] + bind $top <$M1B-O> [cb _next open] + } + + $opts conf -state disabled + + set sorted_recent [_get_recentrepos] + if {[llength $sorted_recent] > 0} { + if {$m_repo ne {}} { + $m_repo add separator + $m_repo add command \ + -state disabled \ + -label [mc "Recent Repositories"] + } + + if {[set lenrecent [llength $sorted_recent]] < $maxrecent} { + set lenrecent $maxrecent + } + + ${NS}::label $w_body.space + ${NS}::label $w_body.recentlabel \ + -anchor w \ + -text [mc "Open Recent Repository:"] + set w_recentlist $w_body.recentlist + text $w_recentlist \ + -cursor $::cursor_ptr \ + -relief flat \ + -background [get_bg_color $w_body.recentlabel] \ + -wrap none \ + -width 50 \ + -height $lenrecent + $w_recentlist tag conf link \ + -foreground blue \ + -underline 1 + set home $::env(HOME) + if {[is_Cygwin]} { + set home [exec cygpath --windows --absolute $home] + } + set home "[file normalize $home]/" + set hlen [string length $home] + foreach p $sorted_recent { + set path $p + if {[string equal -length $hlen $home $p]} { + set p "~/[string range $p $hlen end]" + } + regsub -all "\n" $p "\\n" p + $w_recentlist insert end $p link + $w_recentlist insert end "\n" + + if {$m_repo ne {}} { + $m_repo add command \ + -command [cb _open_recent_path $path] \ + -label " $p" + } + } + $w_recentlist conf -state disabled + $w_recentlist tag bind link <1> [cb _open_recent %x,%y] + pack $w_body.space -anchor w -fill x + pack $w_body.recentlabel -anchor w -fill x + pack $w_recentlist -anchor w -fill x + } + pack $w_body -fill x -padx 10 -pady 10 + + ${NS}::frame $w.buttons + set w_next $w.buttons.next + set w_quit $w.buttons.quit + ${NS}::button $w_quit \ + -text [mc "Quit"] \ + -command exit + pack $w_quit -side right -padx 5 + pack $w.buttons -side bottom -fill x -padx 10 -pady 10 + + if {$m_repo ne {}} { + $m_repo add separator + $m_repo add command \ + -label [mc Quit] \ + -command exit \ + -accelerator $M1T-Q + } + + bind $top <Return> [cb _invoke_next] + bind $top <Visibility> " + [cb _center] + grab $top + focus $top + bind $top <Visibility> {} + " + wm deiconify $top + tkwait variable @done + + grab release $top + if {$top eq {.}} { + eval destroy [winfo children $top] + } +} + +method _center {} { + set nx [winfo reqwidth $top] + set ny [winfo reqheight $top] + set rx [expr {([winfo screenwidth $top] - $nx) / 3}] + set ry [expr {([winfo screenheight $top] - $ny) / 3}] + wm geometry $top [format {+%d+%d} $rx $ry] +} + +method _invoke_next {} { + if {[winfo exists $w_next]} { + uplevel #0 [$w_next cget -command] + } +} + +proc _get_recentrepos {} { + set recent [list] + foreach p [lsort -unique [get_config gui.recentrepo]] { + if {[_is_git [file join $p .git]]} { + lappend recent $p + } else { + _unset_recentrepo $p + } + } + return $recent +} + +proc _unset_recentrepo {p} { + regsub -all -- {([()\[\]{}\.^$+*?\\])} $p {\\\1} p + catch {git config --global --unset-all gui.recentrepo "^$p\$"} + load_config 1 +} + +proc _append_recentrepos {path} { + set path [file normalize $path] + set recent [get_config gui.recentrepo] + + if {[lindex $recent end] eq $path} { + return + } + + set i [lsearch $recent $path] + if {$i >= 0} { + _unset_recentrepo $path + } + + git config --global --add gui.recentrepo $path + load_config 1 + set recent [get_config gui.recentrepo] + + if {[set maxrecent [get_config gui.maxrecentrepo]] eq {}} { + set maxrecent 10 + } + + while {[llength $recent] > $maxrecent} { + _unset_recentrepo [lindex $recent 0] + set recent [get_config gui.recentrepo] + } +} + +method _open_recent {xy} { + set id [lindex [split [$w_recentlist index @$xy] .] 0] + set local_path [lindex $sorted_recent [expr {$id - 1}]] + _do_open2 $this +} + +method _open_recent_path {p} { + set local_path $p + _do_open2 $this +} + +method _next {action} { + global NS + destroy $w_body + if {![winfo exists $w_next]} { + ${NS}::button $w_next -default active + set pos -before + if {[tk windowingsystem] eq "win32"} { set pos -after } + pack $w_next -side right -padx 5 $pos $w_quit + } + _do_$action $this +} + +method _write_local_path {args} { + if {$local_path eq {}} { + $w_next conf -state disabled + } else { + $w_next conf -state normal + } +} + +method _git_init {} { + if {[catch {file mkdir $local_path} err]} { + error_popup [strcat \ + [mc "Failed to create repository %s:" $local_path] \ + "\n\n$err"] + return 0 + } + + if {[catch {cd $local_path} err]} { + error_popup [strcat \ + [mc "Failed to create repository %s:" $local_path] \ + "\n\n$err"] + return 0 + } + + if {[catch {git init} err]} { + error_popup [strcat \ + [mc "Failed to create repository %s:" $local_path] \ + "\n\n$err"] + return 0 + } + + _append_recentrepos [pwd] + set ::_gitdir .git + set ::_prefix {} + return 1 +} + +proc _is_git {path {outdir_var ""}} { + if {$outdir_var ne ""} { + upvar 1 $outdir_var outdir + } + if {[file isfile $path]} { + set fp [open $path r] + gets $fp line + close $fp + if {[regexp "^gitdir: (.+)$" $line line link_target]} { + set path [file join [file dirname $path] $link_target] + set path [file normalize $path] + } + } + + if {[file exists [file join $path HEAD]] + && [file exists [file join $path objects]] + && [file exists [file join $path config]]} { + set outdir $path + return 1 + } + if {[is_Cygwin]} { + if {[file exists [file join $path HEAD]] + && [file exists [file join $path objects.lnk]] + && [file exists [file join $path config.lnk]]} { + set outdir $path + return 1 + } + } + return 0 +} + +proc _objdir {path} { + set objdir [file join $path .git objects] + if {[file isdirectory $objdir]} { + return $objdir + } + + set objdir [file join $path objects] + if {[file isdirectory $objdir]} { + return $objdir + } + + if {[is_Cygwin]} { + set objdir [file join $path .git objects.lnk] + if {[file isfile $objdir]} { + return [win32_read_lnk $objdir] + } + + set objdir [file join $path objects.lnk] + if {[file isfile $objdir]} { + return [win32_read_lnk $objdir] + } + } + + return {} +} + +###################################################################### +## +## Create New Repository + +method _do_new {} { + global use_ttk NS + $w_next conf \ + -state disabled \ + -command [cb _do_new2] \ + -text [mc "Create"] + + ${NS}::frame $w_body + ${NS}::label $w_body.h \ + -font font_uibold -anchor center \ + -text [mc "Create New Repository"] + pack $w_body.h -side top -fill x -pady 10 + pack $w_body -fill x -padx 10 + + ${NS}::frame $w_body.where + ${NS}::label $w_body.where.l -text [mc "Directory:"] + ${NS}::entry $w_body.where.t \ + -textvariable @local_path \ + -width 50 + ${NS}::button $w_body.where.b \ + -text [mc "Browse"] \ + -command [cb _new_local_path] + set w_localpath $w_body.where.t + + grid $w_body.where.l $w_body.where.t $w_body.where.b -sticky ew + pack $w_body.where -fill x + + grid columnconfigure $w_body.where 1 -weight 1 + + trace add variable @local_path write [cb _write_local_path] + bind $w_body.h <Destroy> [list trace remove variable @local_path write [cb _write_local_path]] + update + focus $w_body.where.t +} + +method _new_local_path {} { + if {$local_path ne {}} { + set p [file dirname $local_path] + } else { + set p [pwd] + } + + set p [tk_chooseDirectory \ + -initialdir $p \ + -parent $top \ + -title [mc "Git Repository"] \ + -mustexist false] + if {$p eq {}} return + + set p [file normalize $p] + if {![_new_ok $p]} { + return + } + set local_path $p + $w_localpath icursor end +} + +method _do_new2 {} { + if {![_new_ok $local_path]} { + return + } + if {![_git_init $this]} { + return + } + set done 1 +} + +proc _new_ok {p} { + if {[file isdirectory $p]} { + if {[_is_git [file join $p .git]]} { + error_popup [mc "Directory %s already exists." $p] + return 0 + } + } elseif {[file exists $p]} { + error_popup [mc "File %s already exists." $p] + return 0 + } + return 1 +} + +###################################################################### +## +## Clone Existing Repository + +method _do_clone {} { + global use_ttk NS + $w_next conf \ + -state disabled \ + -command [cb _do_clone2] \ + -text [mc "Clone"] + + ${NS}::frame $w_body + ${NS}::label $w_body.h \ + -font font_uibold -anchor center \ + -text [mc "Clone Existing Repository"] + pack $w_body.h -side top -fill x -pady 10 + pack $w_body -fill x -padx 10 + + set args $w_body.args + ${NS}::frame $w_body.args + pack $args -fill both + + ${NS}::label $args.origin_l -text [mc "Source Location:"] + ${NS}::entry $args.origin_t \ + -textvariable @origin_url \ + -width 50 + ${NS}::button $args.origin_b \ + -text [mc "Browse"] \ + -command [cb _open_origin] + grid $args.origin_l $args.origin_t $args.origin_b -sticky ew + + ${NS}::label $args.where_l -text [mc "Target Directory:"] + ${NS}::entry $args.where_t \ + -textvariable @local_path \ + -width 50 + ${NS}::button $args.where_b \ + -text [mc "Browse"] \ + -command [cb _new_local_path] + grid $args.where_l $args.where_t $args.where_b -sticky ew + set w_localpath $args.where_t + + ${NS}::label $args.type_l -text [mc "Clone Type:"] + ${NS}::frame $args.type_f + set w_types [list] + lappend w_types [${NS}::radiobutton $args.type_f.hardlink \ + -state disabled \ + -text [mc "Standard (Fast, Semi-Redundant, Hardlinks)"] \ + -variable @clone_type \ + -value hardlink] + lappend w_types [${NS}::radiobutton $args.type_f.full \ + -state disabled \ + -text [mc "Full Copy (Slower, Redundant Backup)"] \ + -variable @clone_type \ + -value full] + lappend w_types [${NS}::radiobutton $args.type_f.shared \ + -state disabled \ + -text [mc "Shared (Fastest, Not Recommended, No Backup)"] \ + -variable @clone_type \ + -value shared] + foreach r $w_types { + pack $r -anchor w + } + ${NS}::checkbutton $args.type_f.recursive \ + -text [mc "Recursively clone submodules too"] \ + -variable @recursive \ + -onvalue true -offvalue false + pack $args.type_f.recursive -anchor w + grid $args.type_l $args.type_f -sticky new + + grid columnconfigure $args 1 -weight 1 + + trace add variable @local_path write [cb _update_clone] + trace add variable @origin_url write [cb _update_clone] + bind $w_body.h <Destroy> " + [list trace remove variable @local_path write [cb _update_clone]] + [list trace remove variable @origin_url write [cb _update_clone]] + " + update + focus $args.origin_t +} + +method _open_origin {} { + if {$origin_url ne {} && [file isdirectory $origin_url]} { + set p $origin_url + } else { + set p [pwd] + } + + set p [tk_chooseDirectory \ + -initialdir $p \ + -parent $top \ + -title [mc "Git Repository"] \ + -mustexist true] + if {$p eq {}} return + + set p [file normalize $p] + if {![_is_git [file join $p .git]] && ![_is_git $p]} { + error_popup [mc "Not a Git repository: %s" [file tail $p]] + return + } + set origin_url $p +} + +method _update_clone {args} { + if {$local_path ne {} && $origin_url ne {}} { + $w_next conf -state normal + } else { + $w_next conf -state disabled + } + + if {$origin_url ne {} && + ( [_is_git [file join $origin_url .git]] + || [_is_git $origin_url])} { + set e normal + if {[[lindex $w_types 0] cget -state] eq {disabled}} { + set clone_type hardlink + } + } else { + set e disabled + set clone_type full + } + + foreach r $w_types { + $r conf -state $e + } +} + +method _do_clone2 {} { + if {[file isdirectory $origin_url]} { + set origin_url [file normalize $origin_url] + } + + if {$clone_type eq {hardlink} && ![file isdirectory $origin_url]} { + error_popup [mc "Standard only available for local repository."] + return + } + if {$clone_type eq {shared} && ![file isdirectory $origin_url]} { + error_popup [mc "Shared only available for local repository."] + return + } + + if {$clone_type eq {hardlink} || $clone_type eq {shared}} { + set objdir [_objdir $origin_url] + if {$objdir eq {}} { + error_popup [mc "Not a Git repository: %s" [file tail $origin_url]] + return + } + } + + set giturl $origin_url + if {[is_Cygwin] && [file isdirectory $giturl]} { + set giturl [exec cygpath --unix --absolute $giturl] + if {$clone_type eq {shared}} { + set objdir [exec cygpath --unix --absolute $objdir] + } + } + + if {[file exists $local_path]} { + error_popup [mc "Location %s already exists." $local_path] + return + } + + if {![_git_init $this]} return + set local_path [pwd] + + if {[catch { + git config remote.$origin_name.url $giturl + git config remote.$origin_name.fetch +refs/heads/*:refs/remotes/$origin_name/* + } err]} { + error_popup [strcat [mc "Failed to configure origin"] "\n\n$err"] + return + } + + destroy $w_body $w_next + + switch -exact -- $clone_type { + hardlink { + set o_status [status_bar::two_line $w_body] + pack $w_body -fill x -padx 10 -pady 10 + + set status_op [$o_status start \ + [mc "Counting objects"] \ + [mc "buckets"]] + update + + if {[file exists [file join $objdir info alternates]]} { + set pwd [pwd] + if {[catch { + file mkdir [gitdir objects info] + set f_in [open [file join $objdir info alternates] r] + set f_cp [open [gitdir objects info alternates] w] + fconfigure $f_in -translation binary -encoding binary + fconfigure $f_cp -translation binary -encoding binary + cd $objdir + while {[gets $f_in line] >= 0} { + if {[is_Cygwin]} { + puts $f_cp [exec cygpath --unix --absolute $line] + } else { + puts $f_cp [file normalize $line] + } + } + close $f_in + close $f_cp + cd $pwd + } err]} { + catch {cd $pwd} + _clone_failed $this [mc "Unable to copy objects/info/alternates: %s" $err] + $status_op stop + return + } + } + + set tolink [list] + set buckets [glob \ + -tails \ + -nocomplain \ + -directory [file join $objdir] ??] + set bcnt [expr {[llength $buckets] + 2}] + set bcur 1 + $status_op update $bcur $bcnt + update + + file mkdir [file join .git objects pack] + foreach i [glob -tails -nocomplain \ + -directory [file join $objdir pack] *] { + lappend tolink [file join pack $i] + } + $status_op update [incr bcur] $bcnt + update + + foreach i $buckets { + file mkdir [file join .git objects $i] + foreach j [glob -tails -nocomplain \ + -directory [file join $objdir $i] *] { + lappend tolink [file join $i $j] + } + $status_op update [incr bcur] $bcnt + update + } + $status_op stop + + if {$tolink eq {}} { + info_popup [strcat \ + [mc "Nothing to clone from %s." $origin_url] \ + "\n" \ + [mc "The 'master' branch has not been initialized."] \ + ] + destroy $w_body + set done 1 + return + } + + set i [lindex $tolink 0] + if {[catch { + file link -hard \ + [file join .git objects $i] \ + [file join $objdir $i] + } err]} { + info_popup [mc "Hardlinks are unavailable. Falling back to copying."] + set i [_copy_files $this $objdir $tolink] + } else { + set i [_link_files $this $objdir [lrange $tolink 1 end]] + } + if {!$i} return + + destroy $w_body + + set o_status {} + } + full { + set o_cons [console::embed \ + $w_body \ + [mc "Cloning from %s" $origin_url]] + pack $w_body -fill both -expand 1 -padx 10 + $o_cons exec \ + [list git fetch --no-tags -k $origin_name] \ + [cb _do_clone_tags] + } + shared { + set fd [open [gitdir objects info alternates] w] + fconfigure $fd -translation binary + puts $fd $objdir + close $fd + } + } + + if {$clone_type eq {hardlink} || $clone_type eq {shared}} { + if {![_clone_refs $this]} return + set pwd [pwd] + if {[catch { + cd $origin_url + set HEAD [git rev-parse --verify HEAD^0] + } err]} { + _clone_failed $this [mc "Not a Git repository: %s" [file tail $origin_url]] + return 0 + } + cd $pwd + _do_clone_checkout $this $HEAD + } +} + +method _copy_files {objdir tocopy} { + set status_op [$o_status start \ + [mc "Copying objects"] \ + [mc "KiB"]] + set tot 0 + set cmp 0 + foreach p $tocopy { + incr tot [file size [file join $objdir $p]] + } + foreach p $tocopy { + if {[catch { + set f_in [open [file join $objdir $p] r] + set f_cp [open [file join .git objects $p] w] + fconfigure $f_in -translation binary -encoding binary + fconfigure $f_cp -translation binary -encoding binary + + while {![eof $f_in]} { + incr cmp [fcopy $f_in $f_cp -size 16384] + $status_op update \ + [expr {$cmp / 1024}] \ + [expr {$tot / 1024}] + update + } + + close $f_in + close $f_cp + } err]} { + _clone_failed $this [mc "Unable to copy object: %s" $err] + $status_op stop + return 0 + } + } + $status_op stop + return 1 +} + +method _link_files {objdir tolink} { + set total [llength $tolink] + set status_op [$o_status start \ + [mc "Linking objects"] \ + [mc "objects"]] + for {set i 0} {$i < $total} {} { + set p [lindex $tolink $i] + if {[catch { + file link -hard \ + [file join .git objects $p] \ + [file join $objdir $p] + } err]} { + _clone_failed $this [mc "Unable to hardlink object: %s" $err] + $status_op stop + return 0 + } + + incr i + if {$i % 5 == 0} { + $status_op update $i $total + update + } + } + $status_op stop + return 1 +} + +method _clone_refs {} { + set pwd [pwd] + if {[catch {cd $origin_url} err]} { + error_popup [mc "Not a Git repository: %s" [file tail $origin_url]] + return 0 + } + set fd_in [git_read for-each-ref \ + --tcl \ + {--format=list %(refname) %(objectname) %(*objectname)}] + cd $pwd + + set fd [open [gitdir packed-refs] w] + fconfigure $fd -translation binary + puts $fd "# pack-refs with: peeled" + while {[gets $fd_in line] >= 0} { + set line [eval $line] + set refn [lindex $line 0] + set robj [lindex $line 1] + set tobj [lindex $line 2] + + if {[regsub ^refs/heads/ $refn \ + "refs/remotes/$origin_name/" refn]} { + puts $fd "$robj $refn" + } elseif {[string match refs/tags/* $refn]} { + puts $fd "$robj $refn" + if {$tobj ne {}} { + puts $fd "^$tobj" + } + } + } + close $fd_in + close $fd + return 1 +} + +method _do_clone_tags {ok} { + if {$ok} { + $o_cons exec \ + [list git fetch --tags -k $origin_name] \ + [cb _do_clone_HEAD] + } else { + $o_cons done $ok + _clone_failed $this [mc "Cannot fetch branches and objects. See console output for details."] + } +} + +method _do_clone_HEAD {ok} { + if {$ok} { + $o_cons exec \ + [list git fetch $origin_name HEAD] \ + [cb _do_clone_full_end] + } else { + $o_cons done $ok + _clone_failed $this [mc "Cannot fetch tags. See console output for details."] + } +} + +method _do_clone_full_end {ok} { + $o_cons done $ok + + if {$ok} { + destroy $w_body + + set HEAD {} + if {[file exists [gitdir FETCH_HEAD]]} { + set fd [open [gitdir FETCH_HEAD] r] + while {[gets $fd line] >= 0} { + if {[regexp "^(.{40})\t\t" $line line HEAD]} { + break + } + } + close $fd + } + + catch {git pack-refs} + _do_clone_checkout $this $HEAD + } else { + _clone_failed $this [mc "Cannot determine HEAD. See console output for details."] + } +} + +method _clone_failed {{why {}}} { + if {[catch {file delete -force $local_path} err]} { + set why [strcat \ + $why \ + "\n\n" \ + [mc "Unable to cleanup %s" $local_path] \ + "\n\n" \ + $err] + } + if {$why ne {}} { + update + error_popup [strcat [mc "Clone failed."] "\n" $why] + } +} + +method _do_clone_checkout {HEAD} { + if {$HEAD eq {}} { + info_popup [strcat \ + [mc "No default branch obtained."] \ + "\n" \ + [mc "The 'master' branch has not been initialized."] \ + ] + set done 1 + return + } + if {[catch { + git update-ref HEAD $HEAD^0 + } err]} { + info_popup [strcat \ + [mc "Cannot resolve %s as a commit." $HEAD^0] \ + "\n $err" \ + "\n" \ + [mc "The 'master' branch has not been initialized."] \ + ] + set done 1 + return + } + + set status [status_bar::two_line $w_body] + pack $w_body -fill x -padx 10 -pady 10 + + # We start the status operation here. + # + # This function calls _readtree_wait as a callback. + # + # _readtree_wait in turn either calls _do_clone_submodules directly, + # or calls _postcheckout_wait as a callback which then calls + # _do_clone_submodules. + # + # _do_clone_submodules calls _do_validate_submodule_cloning. + # + # _do_validate_submodule_cloning stops the status operation. + # + # There are no other calls into this chain from other code. + + set o_status_op [$status start \ + [mc "Creating working directory"] \ + [mc "files"]] + + set readtree_err {} + set fd [git_read --stderr read-tree \ + -m \ + -u \ + -v \ + HEAD \ + HEAD \ + ] + fconfigure $fd -blocking 0 -translation binary + fileevent $fd readable [cb _readtree_wait $fd] +} + +method _readtree_wait {fd} { + set buf [read $fd] + $o_status_op update_meter $buf + append readtree_err $buf + + fconfigure $fd -blocking 1 + if {![eof $fd]} { + fconfigure $fd -blocking 0 + return + } + + if {[catch {close $fd}]} { + set err $readtree_err + regsub {^fatal: } $err {} err + error_popup [strcat \ + [mc "Initial file checkout failed."] \ + "\n\n$err"] + return + } + + # -- Run the post-checkout hook. + # + set fd_ph [githook_read post-checkout [string repeat 0 40] \ + [git rev-parse HEAD] 1] + if {$fd_ph ne {}} { + global pch_error + set pch_error {} + fconfigure $fd_ph -blocking 0 -translation binary -eofchar {} + fileevent $fd_ph readable [cb _postcheckout_wait $fd_ph] + } else { + _do_clone_submodules $this + } +} + +method _postcheckout_wait {fd_ph} { + global pch_error + + append pch_error [read $fd_ph] + fconfigure $fd_ph -blocking 1 + if {[eof $fd_ph]} { + if {[catch {close $fd_ph}]} { + hook_failed_popup post-checkout $pch_error 0 + } + unset pch_error + _do_clone_submodules $this + return + } + fconfigure $fd_ph -blocking 0 +} + +method _do_clone_submodules {} { + if {$recursive eq {true}} { + $o_status_op stop + set o_status_op {} + + destroy $w_body + + set o_cons [console::embed \ + $w_body \ + [mc "Cloning submodules"]] + pack $w_body -fill both -expand 1 -padx 10 + $o_cons exec \ + [list git submodule update --init --recursive] \ + [cb _do_validate_submodule_cloning] + } else { + set done 1 + } +} + +method _do_validate_submodule_cloning {ok} { + if {$ok} { + $o_cons done $ok + set done 1 + } else { + _clone_failed $this [mc "Cannot clone submodules."] + } +} + +###################################################################### +## +## Open Existing Repository + +method _do_open {} { + global NS + $w_next conf \ + -state disabled \ + -command [cb _do_open2] \ + -text [mc "Open"] + + ${NS}::frame $w_body + ${NS}::label $w_body.h \ + -font font_uibold -anchor center \ + -text [mc "Open Existing Repository"] + pack $w_body.h -side top -fill x -pady 10 + pack $w_body -fill x -padx 10 + + ${NS}::frame $w_body.where + ${NS}::label $w_body.where.l -text [mc "Repository:"] + ${NS}::entry $w_body.where.t \ + -textvariable @local_path \ + -width 50 + ${NS}::button $w_body.where.b \ + -text [mc "Browse"] \ + -command [cb _open_local_path] + + grid $w_body.where.l $w_body.where.t $w_body.where.b -sticky ew + pack $w_body.where -fill x + + grid columnconfigure $w_body.where 1 -weight 1 + + trace add variable @local_path write [cb _write_local_path] + bind $w_body.h <Destroy> [list trace remove variable @local_path write [cb _write_local_path]] + update + focus $w_body.where.t +} + +method _open_local_path {} { + if {$local_path ne {}} { + set p $local_path + } else { + set p [pwd] + } + + set p [tk_chooseDirectory \ + -initialdir $p \ + -parent $top \ + -title [mc "Git Repository"] \ + -mustexist true] + if {$p eq {}} return + + set p [file normalize $p] + if {![_is_git [file join $p .git]]} { + error_popup [mc "Not a Git repository: %s" [file tail $p]] + return + } + set local_path $p +} + +method _do_open2 {} { + if {![_is_git [file join $local_path .git] actualgit]} { + error_popup [mc "Not a Git repository: %s" [file tail $local_path]] + return + } + + if {[catch {cd $local_path} err]} { + error_popup [strcat \ + [mc "Failed to open repository %s:" $local_path] \ + "\n\n$err"] + return + } + + _append_recentrepos [pwd] + set ::_gitdir $actualgit + set ::_prefix {} + set done 1 +} + +} diff --git a/git-gui/lib/choose_rev.tcl b/git-gui/lib/choose_rev.tcl new file mode 100644 index 0000000..6dae793 --- /dev/null +++ b/git-gui/lib/choose_rev.tcl @@ -0,0 +1,634 @@ +# git-gui revision chooser +# Copyright (C) 2006, 2007 Shawn Pearce + +class choose_rev { + +image create photo ::choose_rev::img_find -data {R0lGODlhEAAQAIYAAPwCBCQmJDw+PBQSFAQCBMza3NTm5MTW1HyChOT29Ozq7MTq7Kze5Kzm7Oz6/NTy9Iza5GzGzKzS1Nzy9Nz29Kzq9HTGzHTK1Lza3AwKDLzu9JTi7HTW5GTCzITO1Mzq7Hza5FTK1ESyvHzKzKzW3DQyNDyqtDw6PIzW5HzGzAT+/Dw+RKyurNTOzMTGxMS+tJSGdATCxHRydLSqpLymnLSijBweHERCRNze3Pz69PTy9Oze1OTSxOTGrMSqlLy+vPTu5OzSvMymjNTGvNS+tMy2pMyunMSefAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAAAALAAAAAAQABAAAAe4gACCAAECA4OIiAIEBQYHBAKJgwIICQoLDA0IkZIECQ4PCxARCwSSAxITFA8VEBYXGBmJAQYLGhUbHB0eH7KIGRIMEBAgISIjJKaIJQQLFxERIialkieUGigpKRoIBCqJKyyLBwvJAioEyoICLS4v6QQwMQQyLuqLli8zNDU2BCf1lN3AkUPHDh49fAQAAEnGD1MCCALZEaSHkIUMBQS8wWMIkSJGhBzBmFEGgRsBUqpMiSgdAD+BAAAh/mhDcmVhdGVkIGJ5IEJNUFRvR0lGIFBybyB2ZXJzaW9uIDIuNQ0KqSBEZXZlbENvciAxOTk3LDE5OTguIEFsbCByaWdodHMgcmVzZXJ2ZWQuDQpodHRwOi8vd3d3LmRldmVsY29yLmNvbQA7} + +field w ; # our megawidget path +field w_list ; # list of currently filtered specs +field w_filter ; # filter entry for $w_list + +field c_expr {}; # current revision expression +field filter ""; # current filter string +field revtype head; # type of revision chosen +field cur_specs [list]; # list of specs for $revtype +field spec_head ; # list of all head specs +field spec_trck ; # list of all tracking branch specs +field spec_tag ; # list of all tag specs +field tip_data ; # array of tip commit info by refname +field log_last ; # array of reflog date by refname + +field tooltip_wm {} ; # Current tooltip toplevel, if open +field tooltip_t {} ; # Text widget in $tooltip_wm +field tooltip_timer {} ; # Current timer event for our tooltip + +proc new {path {title {}}} { + return [_new $path 0 $title] +} + +proc new_unmerged {path {title {}}} { + return [_new $path 1 $title] +} + +constructor _new {path unmerged_only title} { + global current_branch is_detached use_ttk NS + + if {![info exists ::all_remotes]} { + load_all_remotes + } + + set w $path + + if {$title ne {}} { + ${NS}::labelframe $w -text $title + } else { + ${NS}::frame $w + } + bind $w <Destroy> [cb _delete %W] + + if {$is_detached} { + ${NS}::radiobutton $w.detachedhead_r \ + -text [mc "This Detached Checkout"] \ + -value HEAD \ + -variable @revtype + if {!$use_ttk} {$w.detachedhead_r configure -anchor w} + grid $w.detachedhead_r -sticky we -padx {0 5} -columnspan 2 + } + + ${NS}::radiobutton $w.expr_r \ + -text [mc "Revision Expression:"] \ + -value expr \ + -variable @revtype + ${NS}::entry $w.expr_t \ + -width 50 \ + -textvariable @c_expr \ + -validate key \ + -validatecommand [cb _validate %d %S] + grid $w.expr_r $w.expr_t -sticky we -padx {0 5} + + ${NS}::frame $w.types + ${NS}::radiobutton $w.types.head_r \ + -text [mc "Local Branch"] \ + -value head \ + -variable @revtype + pack $w.types.head_r -side left + ${NS}::radiobutton $w.types.trck_r \ + -text [mc "Tracking Branch"] \ + -value trck \ + -variable @revtype + pack $w.types.trck_r -side left + ${NS}::radiobutton $w.types.tag_r \ + -text [mc "Tag"] \ + -value tag \ + -variable @revtype + pack $w.types.tag_r -side left + set w_filter $w.types.filter + ${NS}::entry $w_filter \ + -width 12 \ + -textvariable @filter \ + -validate key \ + -validatecommand [cb _filter %P] + pack $w_filter -side right + pack [${NS}::label $w.types.filter_icon \ + -image ::choose_rev::img_find \ + ] -side right + grid $w.types -sticky we -padx {0 5} -columnspan 2 + + if {$use_ttk} { + ttk::frame $w.list -style SListbox.TFrame -padding 2 + } else { + frame $w.list + } + set w_list $w.list.l + listbox $w_list \ + -font font_diff \ + -width 50 \ + -height 10 \ + -selectmode browse \ + -exportselection false \ + -xscrollcommand [cb _sb_set $w.list.sbx h] \ + -yscrollcommand [cb _sb_set $w.list.sby v] + if {$use_ttk} { + $w_list configure -relief flat -highlightthickness 0 -borderwidth 0 + } + pack $w_list -fill both -expand 1 + grid $w.list -sticky nswe -padx {20 5} -columnspan 2 + bind $w_list <Any-Motion> [cb _show_tooltip @%x,%y] + bind $w_list <Any-Enter> [cb _hide_tooltip] + bind $w_list <Any-Leave> [cb _hide_tooltip] + bind $w_list <Destroy> [cb _hide_tooltip] + + grid columnconfigure $w 1 -weight 1 + if {$is_detached} { + grid rowconfigure $w 3 -weight 1 + } else { + grid rowconfigure $w 2 -weight 1 + } + + trace add variable @revtype write [cb _select] + bind $w_filter <Key-Return> [list focus $w_list]\;break + bind $w_filter <Key-Down> [list focus $w_list] + + set fmt list + append fmt { %(refname)} + append fmt { [list} + append fmt { %(objecttype)} + append fmt { %(objectname)} + append fmt { [concat %(taggername) %(authorname)]} + append fmt { [reformat_date [concat %(taggerdate) %(authordate)]]} + append fmt { %(subject)} + append fmt {] [list} + append fmt { %(*objecttype)} + append fmt { %(*objectname)} + append fmt { %(*authorname)} + append fmt { [reformat_date %(*authordate)]} + append fmt { %(*subject)} + append fmt {]} + set all_refn [list] + set fr_fd [git_read for-each-ref \ + --tcl \ + --sort=-taggerdate \ + --format=$fmt \ + refs/heads \ + refs/remotes \ + refs/tags \ + ] + fconfigure $fr_fd -translation lf -encoding utf-8 + while {[gets $fr_fd line] > 0} { + set line [eval $line] + if {[lindex $line 1 0] eq {tag}} { + if {[lindex $line 2 0] eq {commit}} { + set sha1 [lindex $line 2 1] + } else { + continue + } + } elseif {[lindex $line 1 0] eq {commit}} { + set sha1 [lindex $line 1 1] + } else { + continue + } + set refn [lindex $line 0] + set tip_data($refn) [lrange $line 1 end] + lappend cmt_refn($sha1) $refn + lappend all_refn $refn + } + close $fr_fd + + if {$unmerged_only} { + set fr_fd [git_read rev-list --all ^$::HEAD] + while {[gets $fr_fd sha1] > 0} { + if {[catch {set rlst $cmt_refn($sha1)}]} continue + foreach refn $rlst { + set inc($refn) 1 + } + } + close $fr_fd + } else { + foreach refn $all_refn { + set inc($refn) 1 + } + } + + set spec_head [list] + foreach name [load_all_heads] { + set refn refs/heads/$name + if {[info exists inc($refn)]} { + lappend spec_head [list $name $refn] + } + } + + set spec_trck [list] + foreach spec [all_tracking_branches] { + set refn [lindex $spec 0] + if {[info exists inc($refn)]} { + regsub ^refs/(heads|remotes)/ $refn {} name + lappend spec_trck [concat $name $spec] + } + } + + set spec_tag [list] + foreach name [load_all_tags] { + set refn refs/tags/$name + if {[info exists inc($refn)]} { + lappend spec_tag [list $name $refn] + } + } + + if {$is_detached} { set revtype HEAD + } elseif {[llength $spec_head] > 0} { set revtype head + } elseif {[llength $spec_trck] > 0} { set revtype trck + } elseif {[llength $spec_tag ] > 0} { set revtype tag + } else { set revtype expr + } + + if {$revtype eq {head} && $current_branch ne {}} { + set i 0 + foreach spec $spec_head { + if {[lindex $spec 0] eq $current_branch} { + $w_list selection clear 0 end + $w_list selection set $i + break + } + incr i + } + } + + return $this +} + +method none {text} { + global NS use_ttk + if {![winfo exists $w.none_r]} { + ${NS}::radiobutton $w.none_r \ + -value none \ + -variable @revtype + if {!$use_ttk} {$w.none_r configure -anchor w} + grid $w.none_r -sticky we -padx {0 5} -columnspan 2 + } + $w.none_r configure -text $text +} + +method get {} { + switch -- $revtype { + head - + trck - + tag { + set i [$w_list curselection] + if {$i ne {}} { + return [lindex $cur_specs $i 0] + } else { + return {} + } + } + + HEAD { return HEAD } + expr { return $c_expr } + none { return {} } + default { error "unknown type of revision" } + } +} + +method pick_tracking_branch {} { + set revtype trck +} + +method focus_filter {} { + if {[$w_filter cget -state] eq {normal}} { + focus $w_filter + } +} + +method bind_listbox {event script} { + bind $w_list $event $script +} + +method get_local_branch {} { + if {$revtype eq {head}} { + return [_expr $this] + } else { + return {} + } +} + +method get_tracking_branch {} { + set i [$w_list curselection] + if {$i eq {} || $revtype ne {trck}} { + return {} + } + return [lrange [lindex $cur_specs $i] 1 end] +} + +method get_commit {} { + set e [_expr $this] + if {$e eq {}} { + return {} + } + return [git rev-parse --verify "$e^0"] +} + +method commit_or_die {} { + if {[catch {set new [get_commit $this]} err]} { + + # Cleanup the not-so-friendly error from rev-parse. + # + regsub {^fatal:\s*} $err {} err + if {$err eq {Needed a single revision}} { + set err {} + } + + set top [winfo toplevel $w] + set msg [strcat [mc "Invalid revision: %s" [get $this]] "\n\n$err"] + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $top] \ + -parent $top \ + -message $msg + error $msg + } + return $new +} + +method _expr {} { + switch -- $revtype { + head - + trck - + tag { + set i [$w_list curselection] + if {$i ne {}} { + return [lindex $cur_specs $i 1] + } else { + error [mc "No revision selected."] + } + } + + expr { + if {$c_expr ne {}} { + return $c_expr + } else { + error [mc "Revision expression is empty."] + } + } + HEAD { return HEAD } + none { return {} } + default { error "unknown type of revision" } + } +} + +method _validate {d S} { + if {$d == 1} { + if {[regexp {\s} $S]} { + return 0 + } + if {[string length $S] > 0} { + set revtype expr + } + } + return 1 +} + +method _filter {P} { + if {[regexp {\s} $P]} { + return 0 + } + _rebuild $this $P + return 1 +} + +method _select {args} { + _rebuild $this $filter + focus_filter $this +} + +method _rebuild {pat} { + set ste normal + switch -- $revtype { + head { set new $spec_head } + trck { set new $spec_trck } + tag { set new $spec_tag } + expr - + HEAD - + none { + set new [list] + set ste disabled + } + } + + if {[$w_list cget -state] eq {disabled}} { + $w_list configure -state normal + } + $w_list delete 0 end + + if {$pat ne {}} { + set pat *${pat}* + } + set cur_specs [list] + foreach spec $new { + set txt [lindex $spec 0] + if {$pat eq {} || [string match $pat $txt]} { + lappend cur_specs $spec + $w_list insert end $txt + } + } + if {$cur_specs ne {}} { + $w_list selection clear 0 end + $w_list selection set 0 + } + + if {[$w_filter cget -state] ne $ste} { + $w_list configure -state $ste + $w_filter configure -state $ste + } +} + +method _delete {current} { + if {$current eq $w} { + delete_this + } +} + +method _sb_set {sb orient first last} { + global NS + set old_focus [focus -lastfor $w] + + if {$first == 0 && $last == 1} { + if {[winfo exists $sb]} { + destroy $sb + if {$old_focus ne {}} { + update + focus $old_focus + } + } + return + } + + if {![winfo exists $sb]} { + if {$orient eq {h}} { + ${NS}::scrollbar $sb -orient h -command [list $w_list xview] + pack $sb -fill x -side bottom -before $w_list + } else { + ${NS}::scrollbar $sb -orient v -command [list $w_list yview] + pack $sb -fill y -side right -before $w_list + } + if {$old_focus ne {}} { + update + focus $old_focus + } + } + + catch {$sb set $first $last} +} + +method _show_tooltip {pos} { + if {$tooltip_wm ne {}} { + _open_tooltip $this + } elseif {$tooltip_timer eq {}} { + set tooltip_timer [after 1000 [cb _open_tooltip]] + } +} + +method _open_tooltip {} { + global remote_url + + set tooltip_timer {} + set pos_x [winfo pointerx $w_list] + set pos_y [winfo pointery $w_list] + if {[winfo containing $pos_x $pos_y] ne $w_list} { + _hide_tooltip $this + return + } + + set pos @[join [list \ + [expr {$pos_x - [winfo rootx $w_list]}] \ + [expr {$pos_y - [winfo rooty $w_list]}]] ,] + set lno [$w_list index $pos] + if {$lno eq {}} { + _hide_tooltip $this + return + } + + set spec [lindex $cur_specs $lno] + set refn [lindex $spec 1] + if {$refn eq {}} { + _hide_tooltip $this + return + } + + if {$tooltip_wm eq {}} { + set tooltip_wm [toplevel $w_list.tooltip -borderwidth 1] + catch {wm attributes $tooltip_wm -type tooltip} + wm overrideredirect $tooltip_wm 1 + wm transient $tooltip_wm [winfo toplevel $w_list] + set tooltip_t $tooltip_wm.label + text $tooltip_t \ + -takefocus 0 \ + -highlightthickness 0 \ + -relief flat \ + -borderwidth 0 \ + -wrap none \ + -background lightyellow \ + -foreground black + $tooltip_t tag conf section_header -font font_uibold + bind $tooltip_wm <Escape> [cb _hide_tooltip] + pack $tooltip_t + } else { + $tooltip_t conf -state normal + $tooltip_t delete 0.0 end + } + + set data $tip_data($refn) + if {[lindex $data 0 0] eq {tag}} { + set tag [lindex $data 0] + if {[lindex $data 1 0] eq {commit}} { + set cmit [lindex $data 1] + } else { + set cmit {} + } + } elseif {[lindex $data 0 0] eq {commit}} { + set tag {} + set cmit [lindex $data 0] + } + + $tooltip_t insert end [lindex $spec 0] + set last [_reflog_last $this [lindex $spec 1]] + if {$last ne {}} { + $tooltip_t insert end "\n" + $tooltip_t insert end [mc "Updated"] + $tooltip_t insert end " $last" + } + $tooltip_t insert end "\n" + + if {$tag ne {}} { + $tooltip_t insert end "\n" + $tooltip_t insert end [mc "Tag"] section_header + $tooltip_t insert end " [lindex $tag 1]\n" + $tooltip_t insert end [lindex $tag 2] + $tooltip_t insert end " ([lindex $tag 3])\n" + $tooltip_t insert end [lindex $tag 4] + $tooltip_t insert end "\n" + } + + if {$cmit ne {}} { + $tooltip_t insert end "\n" + $tooltip_t insert end [mc "Commit@@noun"] section_header + $tooltip_t insert end " [lindex $cmit 1]\n" + $tooltip_t insert end [lindex $cmit 2] + $tooltip_t insert end " ([lindex $cmit 3])\n" + $tooltip_t insert end [lindex $cmit 4] + } + + if {[llength $spec] > 2} { + $tooltip_t insert end "\n" + $tooltip_t insert end [mc "Remote"] section_header + $tooltip_t insert end " [lindex $spec 2]\n" + $tooltip_t insert end [mc "URL"] + $tooltip_t insert end " $remote_url([lindex $spec 2])\n" + $tooltip_t insert end [mc "Branch"] + $tooltip_t insert end " [lindex $spec 3]" + } + + $tooltip_t conf -state disabled + _position_tooltip $this +} + +method _reflog_last {name} { + if {[info exists reflog_last($name)]} { + return reflog_last($name) + } + + set last {} + if {[catch {set last [file mtime [gitdir $name]]}] + && ![catch {set g [open [gitdir logs $name] r]}]} { + fconfigure $g -translation binary + while {[gets $g line] >= 0} { + if {[regexp {> ([1-9][0-9]*) } $line line when]} { + set last $when + } + } + close $g + } + + if {$last ne {}} { + set last [format_date $last] + } + set reflog_last($name) $last + return $last +} + +method _position_tooltip {} { + set max_h [lindex [split [$tooltip_t index end] .] 0] + set max_w 0 + for {set i 1} {$i <= $max_h} {incr i} { + set c [lindex [split [$tooltip_t index "$i.0 lineend"] .] 1] + if {$c > $max_w} {set max_w $c} + } + $tooltip_t conf -width $max_w -height $max_h + + set req_w [winfo reqwidth $tooltip_t] + set req_h [winfo reqheight $tooltip_t] + set pos_x [expr {[winfo pointerx .] + 5}] + set pos_y [expr {[winfo pointery .] + 10}] + + set g "${req_w}x${req_h}" + if {[tk windowingsystem] eq "win32" || $pos_x >= 0} {append g +} + append g $pos_x + if {[tk windowingsystem] eq "win32" || $pos_y >= 0} {append g +} + append g $pos_y + + wm geometry $tooltip_wm $g + raise $tooltip_wm +} + +method _hide_tooltip {} { + if {$tooltip_wm ne {}} { + destroy $tooltip_wm + set tooltip_wm {} + } + if {$tooltip_timer ne {}} { + after cancel $tooltip_timer + set tooltip_timer {} + } +} + +} diff --git a/git-gui/lib/chord.tcl b/git-gui/lib/chord.tcl new file mode 100644 index 0000000..e21e7d3 --- /dev/null +++ b/git-gui/lib/chord.tcl @@ -0,0 +1,158 @@ +# Simple Chord for Tcl +# +# A "chord" is a method with more than one entrypoint and only one body, such +# that the body runs only once all the entrypoints have been called by +# different asynchronous tasks. In this implementation, the chord is defined +# dynamically for each invocation. A SimpleChord object is created, supplying +# body script to be run when the chord is completed, and then one or more notes +# are added to the chord. Each note can be called like a proc, and returns +# immediately if the chord isn't yet complete. When the last remaining note is +# called, the body runs before the note returns. +# +# The SimpleChord class has a constructor that takes the body script, and a +# method add_note that returns a note object. Since the body script does not +# run in the context of the procedure that defined it, a mechanism is provided +# for injecting variables into the chord for use by the body script. The +# activation of a note is idempotent; multiple calls have the same effect as +# a simple call. +# +# If you are invoking asynchronous operations with chord notes as completion +# callbacks, and there is a possibility that earlier operations could complete +# before later ones are started, it is a good practice to create a "common" +# note on the chord that prevents it from being complete until you're certain +# you've added all the notes you need. +# +# Example: +# +# # Turn off the UI while running a couple of async operations. +# lock_ui +# +# set chord [SimpleChord::new { +# unlock_ui +# # Note: $notice here is not referenced in the calling scope +# if {$notice} { info_popup $notice } +# } +# +# # Configure a note to keep the chord from completing until +# # all operations have been initiated. +# set common_note [$chord add_note] +# +# # Activate notes in 'after' callbacks to other operations +# set newnote [$chord add_note] +# async_operation $args [list $newnote activate] +# +# # Communicate with the chord body +# if {$condition} { +# # This sets $notice in the same context that the chord body runs in. +# $chord eval { set notice "Something interesting" } +# } +# +# # Activate the common note, making the chord eligible to complete +# $common_note activate +# +# At this point, the chord will complete at some unknown point in the future. +# The common note might have been the first note activated, or the async +# operations might have completed synchronously and the common note is the +# last one, completing the chord before this code finishes, or anything in +# between. The purpose of the chord is to not have to worry about the order. + +# SimpleChord class: +# Represents a procedure that conceptually has multiple entrypoints that must +# all be called before the procedure executes. Each entrypoint is called a +# "note". The chord is only "completed" when all the notes are "activated". +class SimpleChord { + field notes + field body + field is_completed + field eval_ns + + # Constructor: + # set chord [SimpleChord::new {body}] + # Creates a new chord object with the specified body script. The + # body script is evaluated at most once, when a note is activated + # and the chord has no other non-activated notes. + constructor new {i_body} { + set notes [list] + set body $i_body + set is_completed 0 + set eval_ns "[namespace qualifiers $this]::eval" + return $this + } + + # Method: + # $chord eval {script} + # Runs the specified script in the same context (namespace) in which + # the chord body will be evaluated. This can be used to set variable + # values for the chord body to use. + method eval {script} { + namespace eval $eval_ns $script + } + + # Method: + # set note [$chord add_note] + # Adds a new note to the chord, an instance of ChordNote. Raises an + # error if the chord is already completed, otherwise the chord is + # updated so that the new note must also be activated before the + # body is evaluated. + method add_note {} { + if {$is_completed} { error "Cannot add a note to a completed chord" } + + set note [ChordNote::new $this] + + lappend notes $note + + return $note + } + + # This method is for internal use only and is intentionally undocumented. + method notify_note_activation {} { + if {!$is_completed} { + foreach note $notes { + if {![$note is_activated]} { return } + } + + set is_completed 1 + + namespace eval $eval_ns $body + delete_this + } + } +} + +# ChordNote class: +# Represents a note within a chord, providing a way to activate it. When the +# final note of the chord is activated (this can be any note in the chord, +# with all other notes already previously activated in any order), the chord's +# body is evaluated. +class ChordNote { + field chord + field is_activated + + # Constructor: + # Instances of ChordNote are created internally by calling add_note on + # SimpleChord objects. + constructor new {c} { + set chord $c + set is_activated 0 + return $this + } + + # Method: + # [$note is_activated] + # Returns true if this note has already been activated. + method is_activated {} { + return $is_activated + } + + # Method: + # $note activate + # Activates the note, if it has not already been activated, and + # completes the chord if there are no other notes awaiting + # activation. Subsequent calls will have no further effect. + method activate {} { + if {!$is_activated} { + set is_activated 1 + $chord notify_note_activation + } + } +} diff --git a/git-gui/lib/class.tcl b/git-gui/lib/class.tcl new file mode 100644 index 0000000..f08506f --- /dev/null +++ b/git-gui/lib/class.tcl @@ -0,0 +1,194 @@ +# git-gui simple class/object fake-alike +# Copyright (C) 2007 Shawn Pearce + +proc class {class body} { + if {[namespace exists $class]} { + error "class $class already declared" + } + namespace eval $class " + variable __nextid 0 + variable __sealed 0 + variable __field_list {} + variable __field_array + + proc cb {name args} { + upvar this this + concat \[list ${class}::\$name \$this\] \$args + } + " + namespace eval $class $body +} + +proc field {name args} { + set class [uplevel {namespace current}] + variable ${class}::__sealed + variable ${class}::__field_array + + switch [llength $args] { + 0 { set new [list $name] } + 1 { set new [list $name [lindex $args 0]] } + default { error "wrong # args: field name value?" } + } + + if {$__sealed} { + error "class $class is sealed (cannot add new fields)" + } + + if {[catch {set old $__field_array($name)}]} { + variable ${class}::__field_list + lappend __field_list $new + set __field_array($name) 1 + } else { + error "field $name already declared" + } +} + +proc constructor {name params body} { + set class [uplevel {namespace current}] + set ${class}::__sealed 1 + variable ${class}::__field_list + set mbodyc {} + + append mbodyc {set this } $class + append mbodyc {::__o[incr } $class {::__nextid]::__d} \; + append mbodyc {create_this } $class \; + append mbodyc {set __this [namespace qualifiers $this]} \; + + if {$__field_list ne {}} { + append mbodyc {upvar #0} + foreach n $__field_list { + set n [lindex $n 0] + append mbodyc { ${__this}::} $n { } $n + regsub -all @$n\\M $body "\${__this}::$n" body + } + append mbodyc \; + foreach n $__field_list { + if {[llength $n] == 2} { + append mbodyc \ + {set } [lindex $n 0] { } [list [lindex $n 1]] \; + } + } + } + append mbodyc $body + namespace eval $class [list proc $name $params $mbodyc] +} + +proc method {name params body {deleted {}} {del_body {}}} { + set class [uplevel {namespace current}] + set ${class}::__sealed 1 + variable ${class}::__field_list + set params [linsert $params 0 this] + set mbodyc {} + + append mbodyc {set __this [namespace qualifiers $this]} \; + + switch $deleted { + {} {} + ifdeleted { + append mbodyc {if {![namespace exists $__this]} } + append mbodyc \{ $del_body \; return \} \; + } + default { + error "wrong # args: method name args body (ifdeleted body)?" + } + } + + set decl {} + foreach n $__field_list { + set n [lindex $n 0] + if {[regexp -- $n\\M $body]} { + if { [regexp -all -- $n\\M $body] == 1 + && [regexp -all -- \\\$$n\\M $body] == 1 + && [regexp -all -- \\\$$n\\( $body] == 0} { + regsub -all \ + \\\$$n\\M $body \ + "\[set \${__this}::$n\]" body + } else { + append decl { ${__this}::} $n { } $n + regsub -all @$n\\M $body "\${__this}::$n" body + } + } + } + if {$decl ne {}} { + append mbodyc {upvar #0} $decl \; + } + append mbodyc $body + namespace eval $class [list proc $name $params $mbodyc] +} + +proc create_this {class} { + upvar this this + namespace eval [namespace qualifiers $this] [list proc \ + [namespace tail $this] \ + [list name args] \ + "eval \[list ${class}::\$name $this\] \$args" \ + ] +} + +proc delete_this {{t {}}} { + if {$t eq {}} { + upvar this this + set t $this + } + set t [namespace qualifiers $t] + if {[namespace exists $t]} {namespace delete $t} +} + +proc make_dialog {t w args} { + upvar $t top $w pfx this this + global use_ttk + uplevel [linsert $args 0 make_toplevel $t $w] + catch {wm attributes $top -type dialog} + pave_toplevel $pfx +} + +proc make_toplevel {t w args} { + upvar $t top $w pfx this this + + if {[llength $args] % 2} { + error "make_toplevel topvar winvar {options}" + } + set autodelete 1 + foreach {name value} $args { + switch -exact -- $name { + -autodelete {set autodelete $value} + default {error "unsupported option $name"} + } + } + + if {$::root_exists || [winfo ismapped .]} { + regsub -all {::} $this {__} w + set top .$w + set pfx $top + toplevel $top + set ::root_exists 1 + } else { + set top . + set pfx {} + } + + if {$autodelete} { + wm protocol $top WM_DELETE_WINDOW " + [list delete_this $this] + [list destroy $top] + " + } +} + + +## auto_mkindex support for class/constructor/method +## +auto_mkindex_parser::command class {name body} { + variable parser + variable contextStack + set contextStack [linsert $contextStack 0 $name] + $parser eval [list _%@namespace eval $name] $body + set contextStack [lrange $contextStack 1 end] +} +auto_mkindex_parser::command constructor {name args} { + variable index + variable scriptFile + append index [list set auto_index([fullname $name])] \ + [format { [list source [file join $dir %s]]} \ + [file split $scriptFile]] "\n" +} diff --git a/git-gui/lib/commit.tcl b/git-gui/lib/commit.tcl new file mode 100644 index 0000000..b516aa2 --- /dev/null +++ b/git-gui/lib/commit.tcl @@ -0,0 +1,547 @@ +# git-gui misc. commit reading/writing support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc load_last_commit {} { + global HEAD PARENT MERGE_HEAD commit_type ui_comm commit_author + global repo_config + + if {[llength $PARENT] == 0} { + error_popup [mc "There is nothing to amend. + +You are about to create the initial commit. There is no commit before this to amend. +"] + return + } + + repository_state curType curHEAD curMERGE_HEAD + if {$curType eq {merge}} { + error_popup [mc "Cannot amend while merging. + +You are currently in the middle of a merge that has not been fully completed. You cannot amend the prior commit unless you first abort the current merge activity. +"] + return + } + + set msg {} + set parents [list] + if {[catch { + set name "" + set email "" + set fd [git_read cat-file commit $curHEAD] + fconfigure $fd -encoding binary -translation lf + # By default commits are assumed to be in utf-8 + set enc utf-8 + while {[gets $fd line] > 0} { + if {[string match {parent *} $line]} { + lappend parents [string range $line 7 end] + } elseif {[string match {encoding *} $line]} { + set enc [string tolower [string range $line 9 end]] + } elseif {[regexp "author (.*)\\s<(.*)>\\s(\\d.*$)" $line all name email time]} { } + } + set msg [read $fd] + close $fd + + set enc [tcl_encoding $enc] + if {$enc ne {}} { + set msg [encoding convertfrom $enc $msg] + set name [encoding convertfrom $enc $name] + set email [encoding convertfrom $enc $email] + } + if {$name ne {} && $email ne {}} { + set commit_author [list name $name email $email date $time] + } + + set msg [string trim $msg] + } err]} { + error_popup [strcat [mc "Error loading commit data for amend:"] "\n\n$err"] + return + } + + set HEAD $curHEAD + set PARENT $parents + set MERGE_HEAD [list] + switch -- [llength $parents] { + 0 {set commit_type amend-initial} + 1 {set commit_type amend} + default {set commit_type amend-merge} + } + + $ui_comm delete 0.0 end + $ui_comm insert end $msg + $ui_comm edit reset + $ui_comm edit modified false + rescan ui_ready +} + +set GIT_COMMITTER_IDENT {} + +proc committer_ident {} { + global GIT_COMMITTER_IDENT + + if {$GIT_COMMITTER_IDENT eq {}} { + if {[catch {set me [git var GIT_COMMITTER_IDENT]} err]} { + error_popup [strcat [mc "Unable to obtain your identity:"] "\n\n$err"] + return {} + } + if {![regexp {^(.*) [0-9]+ [-+0-9]+$} \ + $me me GIT_COMMITTER_IDENT]} { + error_popup [strcat [mc "Invalid GIT_COMMITTER_IDENT:"] "\n\n$me"] + return {} + } + } + + return $GIT_COMMITTER_IDENT +} + +proc do_signoff {} { + global ui_comm + + set me [committer_ident] + if {$me eq {}} return + + set sob "Signed-off-by: $me" + set last [$ui_comm get {end -1c linestart} {end -1c}] + if {$last ne $sob} { + $ui_comm edit separator + if {$last ne {} + && ![regexp {^[A-Z][A-Za-z]*-[A-Za-z-]+: *} $last]} { + $ui_comm insert end "\n" + } + $ui_comm insert end "\n$sob" + $ui_comm edit separator + $ui_comm see end + } +} + +proc create_new_commit {} { + global commit_type ui_comm commit_author + + set commit_type normal + unset -nocomplain commit_author + $ui_comm delete 0.0 end + $ui_comm edit reset + $ui_comm edit modified false + rescan ui_ready +} + +proc setup_commit_encoding {msg_wt {quiet 0}} { + global repo_config + + if {[catch {set enc $repo_config(i18n.commitencoding)}]} { + set enc utf-8 + } + set use_enc [tcl_encoding $enc] + if {$use_enc ne {}} { + fconfigure $msg_wt -encoding $use_enc + } else { + if {!$quiet} { + error_popup [mc "warning: Tcl does not support encoding '%s'." $enc] + } + fconfigure $msg_wt -encoding utf-8 + } +} + +proc commit_tree {} { + global HEAD commit_type file_states ui_comm repo_config + global pch_error + + if {[committer_ident] eq {}} return + if {![lock_index update]} return + + # -- Our in memory state should match the repository. + # + repository_state curType curHEAD curMERGE_HEAD + if {[string match amend* $commit_type] + && $curType eq {normal} + && $curHEAD eq $HEAD} { + } elseif {$commit_type ne $curType || $HEAD ne $curHEAD} { + info_popup [mc "Last scanned state does not match repository state. + +Another Git program has modified this repository since the last scan. A rescan must be performed before another commit can be created. + +The rescan will be automatically started now. +"] + unlock_index + rescan ui_ready + return + } + + # -- At least one file should differ in the index. + # + set files_ready 0 + foreach path [array names file_states] { + set s $file_states($path) + switch -glob -- [lindex $s 0] { + _? {continue} + A? - + D? - + T? - + M? {set files_ready 1} + _U - + U? { + error_popup [mc "Unmerged files cannot be committed. + +File %s has merge conflicts. You must resolve them and stage the file before committing. +" [short_path $path]] + unlock_index + return + } + default { + error_popup [mc "Unknown file state %s detected. + +File %s cannot be committed by this program. +" [lindex $s 0] [short_path $path]] + } + } + } + if {!$files_ready && ![string match *merge $curType] && ![is_enabled nocommit]} { + info_popup [mc "No changes to commit. + +You must stage at least 1 file before you can commit. +"] + unlock_index + return + } + + if {[is_enabled nocommitmsg]} { do_quit 0 } + + # -- A message is required. + # + set msg [string trim [$ui_comm get 1.0 end]] + regsub -all -line {[ \t\r]+$} $msg {} msg + if {$msg eq {}} { + error_popup [mc "Please supply a commit message. + +A good commit message has the following format: + +- First line: Describe in one sentence what you did. +- Second line: Blank +- Remaining lines: Describe why this change is good. +"] + unlock_index + return + } + + # -- Build the message file. + # + set msg_p [gitdir GITGUI_EDITMSG] + set msg_wt [open $msg_p w] + fconfigure $msg_wt -translation lf + setup_commit_encoding $msg_wt + puts $msg_wt $msg + close $msg_wt + + if {[is_enabled nocommit]} { do_quit 0 } + + # -- Run the pre-commit hook. + # + set fd_ph [githook_read pre-commit] + if {$fd_ph eq {}} { + commit_commitmsg $curHEAD $msg_p + return + } + + ui_status [mc "Calling pre-commit hook..."] + set pch_error {} + fconfigure $fd_ph -blocking 0 -translation binary -eofchar {} + fileevent $fd_ph readable \ + [list commit_prehook_wait $fd_ph $curHEAD $msg_p] +} + +proc commit_prehook_wait {fd_ph curHEAD msg_p} { + global pch_error + + append pch_error [read $fd_ph] + fconfigure $fd_ph -blocking 1 + if {[eof $fd_ph]} { + if {[catch {close $fd_ph}]} { + catch {file delete $msg_p} + ui_status [mc "Commit declined by pre-commit hook."] + hook_failed_popup pre-commit $pch_error + unlock_index + } else { + commit_commitmsg $curHEAD $msg_p + } + set pch_error {} + return + } + fconfigure $fd_ph -blocking 0 +} + +proc commit_commitmsg {curHEAD msg_p} { + global is_detached repo_config + global pch_error + + if {$is_detached + && ![file exists [gitdir rebase-merge head-name]] + && [is_config_true gui.warndetachedcommit]} { + set msg [mc "You are about to commit on a detached head.\ +This is a potentially dangerous thing to do because if you switch\ +to another branch you will lose your changes and it can be difficult\ +to retrieve them later from the reflog. You should probably cancel this\ +commit and create a new branch to continue.\n\ +\n\ +Do you really want to proceed with your Commit?"] + if {[ask_popup $msg] ne yes} { + unlock_index + return + } + } + + # -- Run the commit-msg hook. + # + set fd_ph [githook_read commit-msg $msg_p] + if {$fd_ph eq {}} { + commit_writetree $curHEAD $msg_p + return + } + + ui_status [mc "Calling commit-msg hook..."] + set pch_error {} + fconfigure $fd_ph -blocking 0 -translation binary -eofchar {} + fileevent $fd_ph readable \ + [list commit_commitmsg_wait $fd_ph $curHEAD $msg_p] +} + +proc commit_commitmsg_wait {fd_ph curHEAD msg_p} { + global pch_error + + append pch_error [read $fd_ph] + fconfigure $fd_ph -blocking 1 + if {[eof $fd_ph]} { + if {[catch {close $fd_ph}]} { + catch {file delete $msg_p} + ui_status [mc "Commit declined by commit-msg hook."] + hook_failed_popup commit-msg $pch_error + unlock_index + } else { + commit_writetree $curHEAD $msg_p + } + set pch_error {} + return + } + fconfigure $fd_ph -blocking 0 +} + +proc commit_writetree {curHEAD msg_p} { + ui_status [mc "Committing changes..."] + set fd_wt [git_read write-tree] + fileevent $fd_wt readable \ + [list commit_committree $fd_wt $curHEAD $msg_p] +} + +proc commit_committree {fd_wt curHEAD msg_p} { + global HEAD PARENT MERGE_HEAD commit_type commit_author + global current_branch + global ui_comm commit_type_is_amend + global file_states selected_paths rescan_active + global repo_config + global env + + gets $fd_wt tree_id + if {[catch {close $fd_wt} err]} { + catch {file delete $msg_p} + error_popup [strcat [mc "write-tree failed:"] "\n\n$err"] + ui_status [mc "Commit failed."] + unlock_index + return + } + + # -- Verify this wasn't an empty change. + # + if {$commit_type eq {normal}} { + set fd_ot [git_read cat-file commit $PARENT] + fconfigure $fd_ot -encoding binary -translation lf + set old_tree [gets $fd_ot] + close $fd_ot + + if {[string equal -length 5 {tree } $old_tree] + && [string length $old_tree] == 45} { + set old_tree [string range $old_tree 5 end] + } else { + error [mc "Commit %s appears to be corrupt" $PARENT] + } + + if {$tree_id eq $old_tree} { + catch {file delete $msg_p} + info_popup [mc "No changes to commit. + +No files were modified by this commit and it was not a merge commit. + +A rescan will be automatically started now. +"] + unlock_index + rescan {ui_status [mc "No changes to commit."]} + return + } + } + + if {[info exists commit_author]} { + set old_author [commit_author_ident $commit_author] + } + # -- Create the commit. + # + set cmd [list commit-tree $tree_id] + if {[is_config_true commit.gpgsign]} { + lappend cmd -S + } + foreach p [concat $PARENT $MERGE_HEAD] { + lappend cmd -p $p + } + lappend cmd <$msg_p + if {[catch {set cmt_id [eval git $cmd]} err]} { + catch {file delete $msg_p} + error_popup [strcat [mc "commit-tree failed:"] "\n\n$err"] + ui_status [mc "Commit failed."] + unlock_index + unset -nocomplain commit_author + commit_author_reset $old_author + return + } + if {[info exists commit_author]} { + unset -nocomplain commit_author + commit_author_reset $old_author + } + + # -- Update the HEAD ref. + # + set reflogm commit + if {$commit_type ne {normal}} { + append reflogm " ($commit_type)" + } + set msg_fd [open $msg_p r] + setup_commit_encoding $msg_fd 1 + gets $msg_fd subject + close $msg_fd + append reflogm {: } $subject + if {[catch { + git update-ref -m $reflogm HEAD $cmt_id $curHEAD + } err]} { + catch {file delete $msg_p} + error_popup [strcat [mc "update-ref failed:"] "\n\n$err"] + ui_status [mc "Commit failed."] + unlock_index + return + } + + # -- Cleanup after ourselves. + # + catch {file delete $msg_p} + catch {file delete [gitdir MERGE_HEAD]} + catch {file delete [gitdir MERGE_MSG]} + catch {file delete [gitdir SQUASH_MSG]} + catch {file delete [gitdir GITGUI_MSG]} + catch {file delete [gitdir CHERRY_PICK_HEAD]} + + # -- Let rerere do its thing. + # + if {[get_config rerere.enabled] eq {}} { + set rerere [file isdirectory [gitdir rr-cache]] + } else { + set rerere [is_config_true rerere.enabled] + } + if {$rerere} { + catch {git rerere} + } + + # -- Run the post-commit hook. + # + set fd_ph [githook_read post-commit] + if {$fd_ph ne {}} { + global pch_error + set pch_error {} + fconfigure $fd_ph -blocking 0 -translation binary -eofchar {} + fileevent $fd_ph readable \ + [list commit_postcommit_wait $fd_ph $cmt_id] + } + + $ui_comm delete 0.0 end + $ui_comm edit reset + $ui_comm edit modified false + if {$::GITGUI_BCK_exists} { + catch {file delete [gitdir GITGUI_BCK]} + set ::GITGUI_BCK_exists 0 + } + + if {[is_enabled singlecommit]} { do_quit 0 } + + # -- Update in memory status + # + set commit_type normal + set commit_type_is_amend 0 + set HEAD $cmt_id + set PARENT $cmt_id + set MERGE_HEAD [list] + + foreach path [array names file_states] { + set s $file_states($path) + set m [lindex $s 0] + switch -glob -- $m { + _O - + _M - + _D {continue} + __ - + A_ - + M_ - + T_ - + D_ { + unset file_states($path) + catch {unset selected_paths($path)} + } + DO { + set file_states($path) [list _O [lindex $s 1] {} {}] + } + AM - + AD - + AT - + TM - + TD - + MM - + MT - + MD { + set file_states($path) [list \ + _[string index $m 1] \ + [lindex $s 1] \ + [lindex $s 3] \ + {}] + } + } + } + + display_all_files + unlock_index + reshow_diff + ui_status [mc "Created commit %s: %s" [string range $cmt_id 0 7] $subject] +} + +proc commit_postcommit_wait {fd_ph cmt_id} { + global pch_error + + append pch_error [read $fd_ph] + fconfigure $fd_ph -blocking 1 + if {[eof $fd_ph]} { + if {[catch {close $fd_ph}]} { + hook_failed_popup post-commit $pch_error 0 + } + unset pch_error + return + } + fconfigure $fd_ph -blocking 0 +} + +proc commit_author_ident {details} { + global env + array set author $details + set old [array get env GIT_AUTHOR_*] + set env(GIT_AUTHOR_NAME) $author(name) + set env(GIT_AUTHOR_EMAIL) $author(email) + set env(GIT_AUTHOR_DATE) $author(date) + return $old +} +proc commit_author_reset {details} { + global env + unset env(GIT_AUTHOR_NAME) env(GIT_AUTHOR_EMAIL) env(GIT_AUTHOR_DATE) + if {$details ne {}} { + array set env $details + } +} diff --git a/git-gui/lib/console.tcl b/git-gui/lib/console.tcl new file mode 100644 index 0000000..bb6b9c8 --- /dev/null +++ b/git-gui/lib/console.tcl @@ -0,0 +1,225 @@ +# git-gui console support +# Copyright (C) 2006, 2007 Shawn Pearce + +class console { + +field t_short +field t_long +field w +field w_t +field console_cr +field is_toplevel 1; # are we our own window? + +constructor new {short_title long_title} { + set t_short $short_title + set t_long $long_title + _init $this + return $this +} + +constructor embed {path title} { + set t_short {} + set t_long $title + set w $path + set is_toplevel 0 + _init $this + return $this +} + +method _init {} { + global M1B use_ttk NS + + if {$is_toplevel} { + make_dialog top w -autodelete 0 + wm title $top "[appname] ([reponame]): $t_short" + } else { + ${NS}::frame $w + } + + set console_cr 1.0 + set w_t $w.m.t + + ${NS}::frame $w.m + ${NS}::label $w.m.l1 \ + -textvariable @t_long \ + -anchor w \ + -justify left \ + -font font_uibold + text $w_t \ + -background white \ + -foreground black \ + -borderwidth 1 \ + -relief sunken \ + -width 80 -height 10 \ + -wrap none \ + -font font_diff \ + -state disabled \ + -xscrollcommand [cb _sb_set $w.m.sbx h] \ + -yscrollcommand [cb _sb_set $w.m.sby v] + label $w.m.s -text [mc "Working... please wait..."] \ + -anchor w \ + -justify left \ + -font font_uibold + pack $w.m.l1 -side top -fill x + pack $w.m.s -side bottom -fill x + pack $w_t -side left -fill both -expand 1 + pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10 + + menu $w.ctxm -tearoff 0 + $w.ctxm add command -label [mc "Copy"] \ + -command "tk_textCopy $w_t" + $w.ctxm add command -label [mc "Select All"] \ + -command "focus $w_t;$w_t tag add sel 0.0 end" + $w.ctxm add command -label [mc "Copy All"] \ + -command " + $w_t tag add sel 0.0 end + tk_textCopy $w_t + $w_t tag remove sel 0.0 end + " + + if {$is_toplevel} { + ${NS}::button $w.ok -text [mc "Close"] \ + -state disabled \ + -command [list destroy $w] + pack $w.ok -side bottom -anchor e -pady 10 -padx 10 + bind $w <Visibility> [list focus $w] + } + + bind_button3 $w_t "tk_popup $w.ctxm %X %Y" + bind $w_t <$M1B-Key-a> "$w_t tag add sel 0.0 end;break" + bind $w_t <$M1B-Key-A> "$w_t tag add sel 0.0 end;break" +} + +method exec {cmd {after {}}} { + if {[lindex $cmd 0] eq {git}} { + set fd_f [eval git_read --stderr [lrange $cmd 1 end]] + } else { + lappend cmd 2>@1 + set fd_f [_open_stdout_stderr $cmd] + } + fconfigure $fd_f -blocking 0 -translation binary + fileevent $fd_f readable [cb _read $fd_f $after] +} + +method _read {fd after} { + set buf [read $fd] + if {$buf ne {}} { + if {![winfo exists $w_t]} {_init $this} + $w_t conf -state normal + set c 0 + set n [string length $buf] + while {$c < $n} { + set cr [string first "\r" $buf $c] + set lf [string first "\n" $buf $c] + if {$cr < 0} {set cr [expr {$n + 1}]} + if {$lf < 0} {set lf [expr {$n + 1}]} + + if {$lf < $cr} { + $w_t insert end [string range $buf $c $lf] + set console_cr [$w_t index {end -1c}] + set c $lf + incr c + } else { + $w_t delete $console_cr end + $w_t insert end "\n" + $w_t insert end [string range $buf $c [expr {$cr - 1}]] + set c $cr + incr c + } + } + $w_t conf -state disabled + $w_t see end + } + + fconfigure $fd -blocking 1 + if {[eof $fd]} { + if {[catch {close $fd}]} { + set ok 0 + } else { + set ok 1 + } + if {$after ne {}} { + uplevel #0 $after $ok + } else { + done $this $ok + } + return + } + fconfigure $fd -blocking 0 +} + +method chain {cmdlist {ok 1}} { + if {$ok} { + if {[llength $cmdlist] == 0} { + done $this $ok + return + } + + set cmd [lindex $cmdlist 0] + set cmdlist [lrange $cmdlist 1 end] + + if {[lindex $cmd 0] eq {exec}} { + exec $this \ + [lrange $cmd 1 end] \ + [cb chain $cmdlist] + } else { + uplevel #0 $cmd [cb chain $cmdlist] + } + } else { + done $this $ok + } +} + +method insert {txt} { + if {![winfo exists $w_t]} {_init $this} + $w_t conf -state normal + $w_t insert end "$txt\n" + set console_cr [$w_t index {end -1c}] + $w_t conf -state disabled +} + +method done {ok} { + if {$ok} { + if {[winfo exists $w.m.s]} { + bind $w.m.s <Destroy> [list delete_this $this] + $w.m.s conf -background green -foreground black \ + -text [mc "Success"] + if {$is_toplevel} { + $w.ok conf -state normal + focus $w.ok + } + } else { + delete_this + } + } else { + if {![winfo exists $w.m.s]} { + _init $this + } + bind $w.m.s <Destroy> [list delete_this $this] + $w.m.s conf -background red -foreground black \ + -text [mc "Error: Command Failed"] + if {$is_toplevel} { + $w.ok conf -state normal + focus $w.ok + } + } + + bind $w <Key-Escape> "destroy $w;break" +} + +method _sb_set {sb orient first last} { + global NS + if {![winfo exists $sb]} { + if {$first == $last || ($first == 0 && $last == 1)} return + if {$orient eq {h}} { + ${NS}::scrollbar $sb -orient h -command [list $w_t xview] + pack $sb -fill x -side bottom -before $w_t + } else { + ${NS}::scrollbar $sb -orient v -command [list $w_t yview] + pack $sb -fill y -side right -before $w_t + } + } + $sb set $first $last +} + +} diff --git a/git-gui/lib/database.tcl b/git-gui/lib/database.tcl new file mode 100644 index 0000000..8578308 --- /dev/null +++ b/git-gui/lib/database.tcl @@ -0,0 +1,115 @@ +# git-gui object database management support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc do_stats {} { + global use_ttk NS + set fd [git_read count-objects -v] + while {[gets $fd line] > 0} { + if {[regexp {^([^:]+): (\d+)$} $line _ name value]} { + set stats($name) $value + } + } + close $fd + + set packed_sz 0 + foreach p [glob -directory [gitdir objects pack] \ + -type f \ + -nocomplain -- *] { + incr packed_sz [file size $p] + } + if {$packed_sz > 0} { + set stats(size-pack) [expr {$packed_sz / 1024}] + } + + set w .stats_view + Dialog $w + wm withdraw $w + wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.close -text [mc Close] \ + -default active \ + -command [list destroy $w] + ${NS}::button $w.buttons.gc -text [mc "Compress Database"] \ + -default normal \ + -command "destroy $w;do_gc" + pack $w.buttons.close -side right + pack $w.buttons.gc -side left + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::labelframe $w.stat -text [mc "Database Statistics"] + foreach s { + {count {mc "Number of loose objects"}} + {size {mc "Disk space used by loose objects"} { KiB}} + {in-pack {mc "Number of packed objects"}} + {packs {mc "Number of packs"}} + {size-pack {mc "Disk space used by packed objects"} { KiB}} + {prune-packable {mc "Packed objects waiting for pruning"}} + {garbage {mc "Garbage files"}} + } { + set name [lindex $s 0] + set label [eval [lindex $s 1]] + if {[catch {set value $stats($name)}]} continue + if {[llength $s] > 2} { + set value "$value[lindex $s 2]" + } + + ${NS}::label $w.stat.l_$name -text [mc "%s:" $label] -anchor w + ${NS}::label $w.stat.v_$name -text $value -anchor w + grid $w.stat.l_$name $w.stat.v_$name -sticky we -padx {0 5} + } + pack $w.stat -pady 10 -padx 10 + + bind $w <Visibility> "grab $w; focus $w.buttons.close" + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [list destroy $w] + wm title $w [mc "%s (%s): Database Statistics" [appname] [reponame]] + wm deiconify $w + tkwait window $w +} + +proc do_gc {} { + set w [console::new {gc} [mc "Compressing the object database"]] + console::chain $w { + {exec git pack-refs --prune} + {exec git reflog expire --all} + {exec git repack -a -d -l} + {exec git rerere gc} + } +} + +proc do_fsck_objects {} { + set w [console::new {fsck-objects} \ + [mc "Verifying the object database with fsck-objects"]] + set cmd [list git fsck-objects] + lappend cmd --full + lappend cmd --cache + lappend cmd --strict + console::exec $w $cmd +} + +proc hint_gc {} { + set ndirs 1 + set limit 8 + if {[is_Windows]} { + set ndirs 4 + set limit 1 + } + + set count [llength [glob \ + -nocomplain \ + -- \ + [gitdir objects 4\[0-[expr {$ndirs-1}]\]/*]]] + + if {$count >= $limit * $ndirs} { + set objects_current [expr {$count * 256/$ndirs}] + if {[ask_popup \ + [mc "This repository currently has approximately %i loose objects. + +To maintain optimal performance it is strongly recommended that you compress the database. + +Compress the database now?" $objects_current]] eq yes} { + do_gc + } + } +} diff --git a/git-gui/lib/date.tcl b/git-gui/lib/date.tcl new file mode 100644 index 0000000..abe8299 --- /dev/null +++ b/git-gui/lib/date.tcl @@ -0,0 +1,53 @@ +# git-gui date processing support +# Copyright (C) 2007 Shawn Pearce + +set git_month(Jan) 1 +set git_month(Feb) 2 +set git_month(Mar) 3 +set git_month(Apr) 4 +set git_month(May) 5 +set git_month(Jun) 6 +set git_month(Jul) 7 +set git_month(Aug) 8 +set git_month(Sep) 9 +set git_month(Oct) 10 +set git_month(Nov) 11 +set git_month(Dec) 12 + +proc parse_git_date {s} { + if {$s eq {}} { + return {} + } + + if {![regexp \ + {^... (...) (\d{1,2}) (\d\d):(\d\d):(\d\d) (\d{4}) ([+-]?)(\d\d)(\d\d)$} $s s \ + month day hr mm ss yr ew tz_h tz_m]} { + error [mc "Invalid date from Git: %s" $s] + } + + set s [clock scan [format {%4.4i%2.2i%2.2iT%2s%2s%2s} \ + $yr $::git_month($month) $day \ + $hr $mm $ss] \ + -gmt 1] + + regsub ^0 $tz_h {} tz_h + regsub ^0 $tz_m {} tz_m + switch -- $ew { + - {set ew +} + + {set ew -} + {} {set ew -} + } + + return [expr "$s $ew ($tz_h * 3600 + $tz_m * 60)"] +} + +proc format_date {s} { + if {$s eq {}} { + return {} + } + return [clock format $s -format {%a %b %e %H:%M:%S %Y}] +} + +proc reformat_date {s} { + return [format_date [parse_git_date $s]] +} diff --git a/git-gui/lib/diff.tcl b/git-gui/lib/diff.tcl new file mode 100644 index 0000000..871ad48 --- /dev/null +++ b/git-gui/lib/diff.tcl @@ -0,0 +1,905 @@ +# git-gui diff viewer +# Copyright (C) 2006, 2007 Shawn Pearce + +proc apply_tab_size {{firsttab {}}} { + global have_tk85 repo_config ui_diff + + set w [font measure font_diff "0"] + if {$have_tk85 && $firsttab != 0} { + $ui_diff configure -tabs [list [expr {$firsttab * $w}] [expr {($firsttab + $repo_config(gui.tabsize)) * $w}]] + } elseif {$have_tk85 || $repo_config(gui.tabsize) != 8} { + $ui_diff configure -tabs [expr {$repo_config(gui.tabsize) * $w}] + } else { + $ui_diff configure -tabs {} + } +} + +proc clear_diff {} { + global ui_diff current_diff_path current_diff_header + global ui_index ui_workdir + + $ui_diff conf -state normal + $ui_diff delete 0.0 end + $ui_diff conf -state disabled + + set current_diff_path {} + set current_diff_header {} + + $ui_index tag remove in_diff 0.0 end + $ui_workdir tag remove in_diff 0.0 end +} + +proc reshow_diff {{after {}}} { + global file_states file_lists + global current_diff_path current_diff_side + global ui_diff + + set p $current_diff_path + if {$p eq {}} { + # No diff is being shown. + } elseif {$current_diff_side eq {}} { + clear_diff + } elseif {[catch {set s $file_states($p)}] + || [lsearch -sorted -exact $file_lists($current_diff_side) $p] == -1} { + + if {[find_next_diff $current_diff_side $p {} {[^O]}]} { + next_diff $after + } else { + clear_diff + } + } else { + set save_pos [lindex [$ui_diff yview] 0] + show_diff $p $current_diff_side {} $save_pos $after + } +} + +proc force_diff_encoding {enc} { + global current_diff_path + + if {$current_diff_path ne {}} { + force_path_encoding $current_diff_path $enc + reshow_diff + } +} + +proc handle_empty_diff {} { + global current_diff_path file_states file_lists + global diff_empty_count + + set path $current_diff_path + set s $file_states($path) + if {[lindex $s 0] ne {_M} || [has_textconv $path]} return + + # Prevent infinite rescan loops + incr diff_empty_count + if {$diff_empty_count > 1} return + + info_popup [mc "No differences detected. + +%s has no changes. + +The modification date of this file was updated by another application, but the content within the file was not changed. + +A rescan will be automatically started to find other files which may have the same state." [short_path $path]] + + clear_diff + display_file $path __ + rescan ui_ready 0 +} + +proc show_diff {path w {lno {}} {scroll_pos {}} {callback {}}} { + global file_states file_lists + global is_3way_diff is_conflict_diff diff_active repo_config + global ui_diff ui_index ui_workdir + global current_diff_path current_diff_side current_diff_header + global current_diff_queue + + if {$diff_active || ![lock_index read]} return + + clear_diff + if {$lno == {}} { + set lno [lsearch -sorted -exact $file_lists($w) $path] + if {$lno >= 0} { + incr lno + } + } + if {$lno >= 1} { + $w tag add in_diff $lno.0 [expr {$lno + 1}].0 + $w see $lno.0 + } + + set s $file_states($path) + set m [lindex $s 0] + set is_conflict_diff 0 + set current_diff_path $path + set current_diff_side $w + set current_diff_queue {} + ui_status [mc "Loading diff of %s..." [escape_path $path]] + + set cont_info [list $scroll_pos $callback] + + apply_tab_size 0 + + if {[string first {U} $m] >= 0} { + merge_load_stages $path [list show_unmerged_diff $cont_info] + } elseif {$m eq {_O}} { + show_other_diff $path $w $m $cont_info + } else { + start_show_diff $cont_info + } + + global current_diff_path selected_paths + set selected_paths($current_diff_path) 1 +} + +proc show_unmerged_diff {cont_info} { + global current_diff_path current_diff_side + global merge_stages ui_diff is_conflict_diff + global current_diff_queue + + if {$merge_stages(2) eq {}} { + set is_conflict_diff 1 + lappend current_diff_queue \ + [list [mc "LOCAL: deleted\nREMOTE:\n"] d= \ + [list ":1:$current_diff_path" ":3:$current_diff_path"]] + } elseif {$merge_stages(3) eq {}} { + set is_conflict_diff 1 + lappend current_diff_queue \ + [list [mc "REMOTE: deleted\nLOCAL:\n"] d= \ + [list ":1:$current_diff_path" ":2:$current_diff_path"]] + } elseif {[lindex $merge_stages(1) 0] eq {120000} + || [lindex $merge_stages(2) 0] eq {120000} + || [lindex $merge_stages(3) 0] eq {120000}} { + set is_conflict_diff 1 + lappend current_diff_queue \ + [list [mc "LOCAL:\n"] d= \ + [list ":1:$current_diff_path" ":2:$current_diff_path"]] + lappend current_diff_queue \ + [list [mc "REMOTE:\n"] d= \ + [list ":1:$current_diff_path" ":3:$current_diff_path"]] + } else { + start_show_diff $cont_info + return + } + + advance_diff_queue $cont_info +} + +proc advance_diff_queue {cont_info} { + global current_diff_queue ui_diff + + set item [lindex $current_diff_queue 0] + set current_diff_queue [lrange $current_diff_queue 1 end] + + $ui_diff conf -state normal + $ui_diff insert end [lindex $item 0] [lindex $item 1] + $ui_diff conf -state disabled + + start_show_diff $cont_info [lindex $item 2] +} + +proc show_other_diff {path w m cont_info} { + global file_states file_lists + global is_3way_diff diff_active repo_config + global ui_diff ui_index ui_workdir + global current_diff_path current_diff_side current_diff_header + + # - Git won't give us the diff, there's nothing to compare to! + # + if {$m eq {_O}} { + set max_sz 100000 + set type unknown + if {[catch { + set type [file type $path] + switch -- $type { + directory { + set type submodule + set content {} + set sz 0 + } + link { + set content [file readlink $path] + set sz [string length $content] + } + file { + set fd [open $path r] + fconfigure $fd \ + -eofchar {} \ + -encoding [get_path_encoding $path] + set content [read $fd $max_sz] + close $fd + set sz [file size $path] + } + default { + error "'$type' not supported" + } + } + } err ]} { + set diff_active 0 + unlock_index + ui_status [mc "Unable to display %s" [escape_path $path]] + error_popup [strcat [mc "Error loading file:"] "\n\n$err"] + return + } + $ui_diff conf -state normal + if {$type eq {submodule}} { + $ui_diff insert end \ + "* [mc "Git Repository (subproject)"]\n" \ + d_info + } elseif {![catch {set type [exec file $path]}]} { + set n [string length $path] + if {[string equal -length $n $path $type]} { + set type [string range $type $n end] + regsub {^:?\s*} $type {} type + } + $ui_diff insert end "* $type\n" d_info + } + if {[string first "\0" $content] != -1} { + $ui_diff insert end \ + [mc "* Binary file (not showing content)."] \ + d_info + } else { + if {$sz > $max_sz} { + $ui_diff insert end [mc \ +"* Untracked file is %d bytes. +* Showing only first %d bytes. +" $sz $max_sz] d_info + } + $ui_diff insert end $content + if {$sz > $max_sz} { + $ui_diff insert end [mc " +* Untracked file clipped here by %s. +* To see the entire file, use an external editor. +" [appname]] d_info + } + } + $ui_diff conf -state disabled + set diff_active 0 + unlock_index + set scroll_pos [lindex $cont_info 0] + if {$scroll_pos ne {}} { + update + $ui_diff yview moveto $scroll_pos + } + ui_ready + set callback [lindex $cont_info 1] + if {$callback ne {}} { + eval $callback + } + return + } +} + +proc start_show_diff {cont_info {add_opts {}}} { + global file_states file_lists + global is_3way_diff is_submodule_diff diff_active repo_config + global ui_diff ui_index ui_workdir + global current_diff_path current_diff_side current_diff_header + + set path $current_diff_path + set w $current_diff_side + + set s $file_states($path) + set m [lindex $s 0] + set is_3way_diff 0 + set is_submodule_diff 0 + set diff_active 1 + set current_diff_header {} + set conflict_size [gitattr $path conflict-marker-size 7] + + set cmd [list] + if {$w eq $ui_index} { + lappend cmd diff-index + lappend cmd --cached + if {[git-version >= "1.7.2"]} { + lappend cmd --ignore-submodules=dirty + } + } elseif {$w eq $ui_workdir} { + if {[string first {U} $m] >= 0} { + lappend cmd diff + } else { + lappend cmd diff-files + } + } + if {![is_config_false gui.textconv] && [git-version >= 1.6.1]} { + lappend cmd --textconv + } + + if {[string match {160000 *} [lindex $s 2]] + || [string match {160000 *} [lindex $s 3]]} { + set is_submodule_diff 1 + + if {[git-version >= "1.6.6"]} { + lappend cmd --submodule + } + } + + lappend cmd -p + lappend cmd --color + set cmd [concat $cmd $repo_config(gui.diffopts)] + if {$repo_config(gui.diffcontext) >= 1} { + lappend cmd "-U$repo_config(gui.diffcontext)" + } + if {$w eq $ui_index} { + lappend cmd [PARENT] + } + if {$add_opts ne {}} { + eval lappend cmd $add_opts + } else { + lappend cmd -- + lappend cmd $path + } + + if {$is_submodule_diff && [git-version < "1.6.6"]} { + if {$w eq $ui_index} { + set cmd [list submodule summary --cached -- $path] + } else { + set cmd [list submodule summary --files -- $path] + } + } + + if {[catch {set fd [eval git_read --nice $cmd]} err]} { + set diff_active 0 + unlock_index + ui_status [mc "Unable to display %s" [escape_path $path]] + error_popup [strcat [mc "Error loading diff:"] "\n\n$err"] + return + } + + set ::current_diff_inheader 1 + # Detect pre-image lines of the diff3 conflict-style. They are just + # '++' lines which is not bijective. Thus, we need to maintain a state + # across lines. + set ::conflict_in_pre_image 0 + fconfigure $fd \ + -blocking 0 \ + -encoding [get_path_encoding $path] \ + -translation lf + fileevent $fd readable [list read_diff $fd $conflict_size $cont_info] +} + +proc parse_color_line {line} { + set start 0 + set result "" + set markup [list] + set regexp {\033\[((?:\d+;)*\d+)?m} + set need_reset 0 + while {[regexp -indices -start $start $regexp $line match code]} { + foreach {begin end} $match break + append result [string range $line $start [expr {$begin - 1}]] + set pos [string length $result] + set col [eval [linsert $code 0 string range $line]] + set start [incr end] + if {$col eq "0" || $col eq ""} { + if {!$need_reset} continue + set need_reset 0 + } else { + set need_reset 1 + } + lappend markup $pos $col + } + append result [string range $line $start end] + if {[llength $markup] < 4} {set markup {}} + return [list $result $markup] +} + +proc read_diff {fd conflict_size cont_info} { + global ui_diff diff_active is_submodule_diff + global is_3way_diff is_conflict_diff current_diff_header + global current_diff_queue + global diff_empty_count + + $ui_diff conf -state normal + while {[gets $fd line] >= 0} { + foreach {line markup} [parse_color_line $line] break + set line [string map {\033 ^} $line] + + set tags {} + + # -- Check for start of diff header. + if { [string match {diff --git *} $line] + || [string match {diff --cc *} $line] + || [string match {diff --combined *} $line]} { + set ::current_diff_inheader 1 + } + + # -- Check for end of diff header (any hunk line will do this). + # + if {[regexp {^@@+ } $line]} {set ::current_diff_inheader 0} + + # -- Automatically detect if this is a 3 way diff. + # + if {[string match {@@@ *} $line]} { + set is_3way_diff 1 + apply_tab_size 1 + } + + if {$::current_diff_inheader} { + + # -- These two lines stop a diff header and shouldn't be in there + if { [string match {Binary files * and * differ} $line] + || [regexp {^\* Unmerged path } $line]} { + set ::current_diff_inheader 0 + } else { + append current_diff_header $line "\n" + } + + # -- Cleanup uninteresting diff header lines. + # + if { [string match {diff --git *} $line] + || [string match {diff --cc *} $line] + || [string match {diff --combined *} $line] + || [string match {--- *} $line] + || [string match {+++ *} $line] + || [string match {index *} $line]} { + continue + } + + # -- Name it symlink, not 120000 + # Note, that the original line is in $current_diff_header + regsub {^(deleted|new) file mode 120000} $line {\1 symlink} line + + } elseif { $line eq {\ No newline at end of file}} { + # -- Handle some special lines + } elseif {$is_3way_diff} { + set op [string range $line 0 1] + switch -- $op { + { } {set tags {}} + {@@} {set tags d_@} + { +} {set tags d_s+} + { -} {set tags d_s-} + {+ } {set tags d_+s} + {- } {set tags d_-s} + {--} {set tags d_--} + {++} { + set regexp [string map [list %conflict_size $conflict_size]\ + {^\+\+([<>=|]){%conflict_size}(?: |$)}] + if {[regexp $regexp $line _g op]} { + set is_conflict_diff 1 + set line [string replace $line 0 1 { }] + set tags d$op + + # The ||| conflict-marker marks the start of the pre-image. + # All those lines are also prefixed with '++'. Thus we need + # to maintain this state. + set ::conflict_in_pre_image [expr {$op eq {|}}] + } elseif {$::conflict_in_pre_image} { + # This is a pre-image line. It is the one which both sides + # are based on. As it has also the '++' line start, it is + # normally shown as 'added'. Invert this to '--' to make + # it a 'removed' line. + set line [string replace $line 0 1 {--}] + set tags d_-- + } else { + set tags d_++ + } + } + default { + puts "error: Unhandled 3 way diff marker: {$op}" + set tags {} + } + } + } elseif {$is_submodule_diff} { + if {$line == ""} continue + if {[regexp {^Submodule } $line]} { + set tags d_info + } elseif {[regexp {^\* } $line]} { + set line [string replace $line 0 1 {Submodule }] + set tags d_info + } else { + set op [string range $line 0 2] + switch -- $op { + { <} {set tags d_-} + { >} {set tags d_+} + { W} {set tags {}} + default { + puts "error: Unhandled submodule diff marker: {$op}" + set tags {} + } + } + } + } else { + set op [string index $line 0] + switch -- $op { + { } {set tags {}} + {@} {set tags d_@} + {-} {set tags d_-} + {+} { + set regexp [string map [list %conflict_size $conflict_size]\ + {^\+([<>=]){%conflict_size}(?: |$)}] + if {[regexp $regexp $line _g op]} { + set is_conflict_diff 1 + set tags d$op + } else { + set tags d_+ + } + } + default { + puts "error: Unhandled 2 way diff marker: {$op}" + set tags {} + } + } + } + set mark [$ui_diff index "end - 1 line linestart"] + $ui_diff insert end $line $tags + if {[string index $line end] eq "\r"} { + $ui_diff tag add d_cr {end - 2c} + } + $ui_diff insert end "\n" $tags + + foreach {posbegin colbegin posend colend} $markup { + set prefix clr + foreach style [lsort -integer [split $colbegin ";"]] { + if {$style eq "7"} {append prefix i; continue} + if {$style != 4 && ($style < 30 || $style > 47)} {continue} + set a "$mark linestart + $posbegin chars" + set b "$mark linestart + $posend chars" + catch {$ui_diff tag add $prefix$style $a $b} + } + } + } + $ui_diff conf -state disabled + + if {[eof $fd]} { + close $fd + + if {$current_diff_queue ne {}} { + advance_diff_queue $cont_info + return + } + + set diff_active 0 + unlock_index + set scroll_pos [lindex $cont_info 0] + if {$scroll_pos ne {}} { + update + $ui_diff yview moveto $scroll_pos + } + ui_ready + + if {[$ui_diff index end] eq {2.0}} { + handle_empty_diff + } else { + set diff_empty_count 0 + } + + set callback [lindex $cont_info 1] + if {$callback ne {}} { + eval $callback + } + } +} + +proc apply_or_revert_hunk {x y revert} { + global current_diff_path current_diff_header current_diff_side + global ui_diff ui_index file_states last_revert last_revert_enc + + if {$current_diff_path eq {} || $current_diff_header eq {}} return + if {![lock_index apply_hunk]} return + + set apply_cmd {apply --whitespace=nowarn} + set mi [lindex $file_states($current_diff_path) 0] + if {$current_diff_side eq $ui_index} { + set failed_msg [mc "Failed to unstage selected hunk."] + lappend apply_cmd --reverse --cached + if {[string index $mi 0] ne {M}} { + unlock_index + return + } + } else { + if {$revert} { + set failed_msg [mc "Failed to revert selected hunk."] + lappend apply_cmd --reverse + } else { + set failed_msg [mc "Failed to stage selected hunk."] + lappend apply_cmd --cached + } + + if {[string index $mi 1] ne {M}} { + unlock_index + return + } + } + + set s_lno [lindex [split [$ui_diff index @$x,$y] .] 0] + set s_lno [$ui_diff search -backwards -regexp ^@@ $s_lno.0 0.0] + if {$s_lno eq {}} { + unlock_index + return + } + + set e_lno [$ui_diff search -forwards -regexp ^@@ "$s_lno + 1 lines" end] + if {$e_lno eq {}} { + set e_lno end + } + + set wholepatch "$current_diff_header[$ui_diff get $s_lno $e_lno]" + + if {[catch { + set enc [get_path_encoding $current_diff_path] + set p [eval git_write $apply_cmd] + fconfigure $p -translation binary -encoding $enc + puts -nonewline $p $wholepatch + close $p} err]} { + error_popup "$failed_msg\n\n$err" + unlock_index + return + } + + if {$revert} { + # Save a copy of this patch for undoing reverts. + set last_revert $wholepatch + set last_revert_enc $enc + } + + $ui_diff conf -state normal + $ui_diff delete $s_lno $e_lno + $ui_diff conf -state disabled + + # Check if the hunk was the last one in the file. + if {[$ui_diff get 1.0 end] eq "\n"} { + set o _ + } else { + set o ? + } + + # Update the status flags. + if {$revert} { + set mi [string index $mi 0]$o + } elseif {$current_diff_side eq $ui_index} { + set mi ${o}M + } elseif {[string index $mi 0] eq {_}} { + set mi M$o + } else { + set mi ?$o + } + unlock_index + display_file $current_diff_path $mi + # This should trigger shift to the next changed file + if {$o eq {_}} { + reshow_diff + } +} + +proc apply_or_revert_range_or_line {x y revert} { + global current_diff_path current_diff_header current_diff_side + global ui_diff ui_index file_states last_revert + + set selected [$ui_diff tag nextrange sel 0.0] + + if {$selected == {}} { + set first [$ui_diff index "@$x,$y"] + set last $first + } else { + set first [lindex $selected 0] + set last [lindex $selected 1] + } + + set first_l [$ui_diff index "$first linestart"] + set last_l [$ui_diff index "$last lineend"] + + if {$current_diff_path eq {} || $current_diff_header eq {}} return + if {![lock_index apply_hunk]} return + + set apply_cmd {apply --whitespace=nowarn} + set mi [lindex $file_states($current_diff_path) 0] + if {$current_diff_side eq $ui_index} { + set failed_msg [mc "Failed to unstage selected line."] + set to_context {+} + lappend apply_cmd --reverse --cached + if {[string index $mi 0] ne {M}} { + unlock_index + return + } + } else { + if {$revert} { + set failed_msg [mc "Failed to revert selected line."] + set to_context {+} + lappend apply_cmd --reverse + } else { + set failed_msg [mc "Failed to stage selected line."] + set to_context {-} + lappend apply_cmd --cached + } + + if {[string index $mi 1] ne {M}} { + unlock_index + return + } + } + + set wholepatch {} + + while {$first_l < $last_l} { + set i_l [$ui_diff search -backwards -regexp ^@@ $first_l 0.0] + if {$i_l eq {}} { + # If there's not a @@ above, then the selected range + # must have come before the first_l @@ + set i_l [$ui_diff search -regexp ^@@ $first_l $last_l] + } + if {$i_l eq {}} { + unlock_index + return + } + # $i_l is now at the beginning of a line + + # pick start line number from hunk header + set hh [$ui_diff get $i_l "$i_l + 1 lines"] + set hh [lindex [split $hh ,] 0] + set hln [lindex [split $hh -] 1] + set hln [lindex [split $hln " "] 0] + + # There is a special situation to take care of. Consider this + # hunk: + # + # @@ -10,4 +10,4 @@ + # context before + # -old 1 + # -old 2 + # +new 1 + # +new 2 + # context after + # + # We used to keep the context lines in the order they appear in + # the hunk. But then it is not possible to correctly stage only + # "-old 1" and "+new 1" - it would result in this staged text: + # + # context before + # old 2 + # new 1 + # context after + # + # (By symmetry it is not possible to *un*stage "old 2" and "new + # 2".) + # + # We resolve the problem by introducing an asymmetry, namely, + # when a "+" line is *staged*, it is moved in front of the + # context lines that are generated from the "-" lines that are + # immediately before the "+" block. That is, we construct this + # patch: + # + # @@ -10,4 +10,5 @@ + # context before + # +new 1 + # old 1 + # old 2 + # context after + # + # But we do *not* treat "-" lines that are *un*staged in a + # special way. + # + # With this asymmetry it is possible to stage the change "old + # 1" -> "new 1" directly, and to stage the change "old 2" -> + # "new 2" by first staging the entire hunk and then unstaging + # the change "old 1" -> "new 1". + # + # Applying multiple lines adds complexity to the special + # situation. The pre_context must be moved after the entire + # first block of consecutive staged "+" lines, so that + # staging both additions gives the following patch: + # + # @@ -10,4 +10,6 @@ + # context before + # +new 1 + # +new 2 + # old 1 + # old 2 + # context after + + # This is non-empty if and only if we are _staging_ changes; + # then it accumulates the consecutive "-" lines (after + # converting them to context lines) in order to be moved after + # "+" change lines. + set pre_context {} + + set n 0 + set m 0 + set i_l [$ui_diff index "$i_l + 1 lines"] + set patch {} + while {[$ui_diff compare $i_l < "end - 1 chars"] && + [$ui_diff get $i_l "$i_l + 2 chars"] ne {@@}} { + set next_l [$ui_diff index "$i_l + 1 lines"] + set c1 [$ui_diff get $i_l] + if {[$ui_diff compare $first_l <= $i_l] && + [$ui_diff compare $i_l < $last_l] && + ($c1 eq {-} || $c1 eq {+})} { + # a line to stage/unstage + set ln [$ui_diff get $i_l $next_l] + if {$c1 eq {-}} { + set n [expr $n+1] + set patch "$patch$pre_context$ln" + set pre_context {} + } else { + set m [expr $m+1] + set patch "$patch$ln" + } + } elseif {$c1 ne {-} && $c1 ne {+}} { + # context line + set ln [$ui_diff get $i_l $next_l] + set patch "$patch$pre_context$ln" + # Skip the "\ No newline at end of + # file". Depending on the locale setting + # we don't know what this line looks + # like exactly. The only thing we do + # know is that it starts with "\ " + if {![string match {\\ *} $ln]} { + set n [expr $n+1] + set m [expr $m+1] + } + set pre_context {} + } elseif {$c1 eq $to_context} { + # turn change line into context line + set ln [$ui_diff get "$i_l + 1 chars" $next_l] + if {$c1 eq {-}} { + set pre_context "$pre_context $ln" + } else { + set patch "$patch $ln" + } + set n [expr $n+1] + set m [expr $m+1] + } else { + # a change in the opposite direction of + # to_context which is outside the range of + # lines to apply. + set patch "$patch$pre_context" + set pre_context {} + } + set i_l $next_l + } + set patch "$patch$pre_context" + set wholepatch "$wholepatch@@ -$hln,$n +$hln,$m @@\n$patch" + set first_l [$ui_diff index "$next_l + 1 lines"] + } + + if {[catch { + set enc [get_path_encoding $current_diff_path] + set p [eval git_write $apply_cmd] + fconfigure $p -translation binary -encoding $enc + puts -nonewline $p $current_diff_header + puts -nonewline $p $wholepatch + close $p} err]} { + error_popup "$failed_msg\n\n$err" + unlock_index + return + } + + if {$revert} { + # Save a copy of this patch for undoing reverts. + set last_revert $current_diff_header$wholepatch + set last_revert_enc $enc + } + + unlock_index +} + +# Undo the last line/hunk reverted. When hunks and lines are reverted, a copy +# of the diff applied is saved. Re-apply that diff to undo the revert. +# +# Right now, we only use a single variable to hold the copy, and not a +# stack/deque for simplicity, so multiple undos are not possible. Maybe this +# can be added if the need for something like this is felt in the future. +proc undo_last_revert {} { + global last_revert current_diff_path current_diff_header + global last_revert_enc + + if {$last_revert eq {}} return + if {![lock_index apply_hunk]} return + + set apply_cmd {apply --whitespace=nowarn} + set failed_msg [mc "Failed to undo last revert."] + + if {[catch { + set enc $last_revert_enc + set p [eval git_write $apply_cmd] + fconfigure $p -translation binary -encoding $enc + puts -nonewline $p $last_revert + close $p} err]} { + error_popup "$failed_msg\n\n$err" + unlock_index + return + } + + set last_revert {} + + unlock_index +} diff --git a/git-gui/lib/encoding.tcl b/git-gui/lib/encoding.tcl new file mode 100644 index 0000000..32668fc --- /dev/null +++ b/git-gui/lib/encoding.tcl @@ -0,0 +1,466 @@ +# git-gui encoding support +# Copyright (C) 2005 Paul Mackerras <paulus@samba.org> +# (Copied from gitk, commit fd8ccbec4f0161) + +# This list of encoding names and aliases is distilled from +# http://www.iana.org/assignments/character-sets. +# Not all of them are supported by Tcl. +set encoding_aliases { + { ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ASCII + ISO646-US US-ASCII us IBM367 cp367 csASCII } + { ISO-10646-UTF-1 csISO10646UTF1 } + { ISO_646.basic:1983 ref csISO646basic1983 } + { INVARIANT csINVARIANT } + { ISO_646.irv:1983 iso-ir-2 irv csISO2IntlRefVersion } + { BS_4730 iso-ir-4 ISO646-GB gb uk csISO4UnitedKingdom } + { NATS-SEFI iso-ir-8-1 csNATSSEFI } + { NATS-SEFI-ADD iso-ir-8-2 csNATSSEFIADD } + { NATS-DANO iso-ir-9-1 csNATSDANO } + { NATS-DANO-ADD iso-ir-9-2 csNATSDANOADD } + { SEN_850200_B iso-ir-10 FI ISO646-FI ISO646-SE se csISO10Swedish } + { SEN_850200_C iso-ir-11 ISO646-SE2 se2 csISO11SwedishForNames } + { KS_C_5601-1987 iso-ir-149 KS_C_5601-1989 KSC_5601 korean csKSC56011987 } + { ISO-2022-KR csISO2022KR } + { EUC-KR csEUCKR } + { ISO-2022-JP csISO2022JP } + { ISO-2022-JP-2 csISO2022JP2 } + { JIS_C6220-1969-jp JIS_C6220-1969 iso-ir-13 katakana x0201-7 + csISO13JISC6220jp } + { JIS_C6220-1969-ro iso-ir-14 jp ISO646-JP csISO14JISC6220ro } + { IT iso-ir-15 ISO646-IT csISO15Italian } + { PT iso-ir-16 ISO646-PT csISO16Portuguese } + { ES iso-ir-17 ISO646-ES csISO17Spanish } + { greek7-old iso-ir-18 csISO18Greek7Old } + { latin-greek iso-ir-19 csISO19LatinGreek } + { DIN_66003 iso-ir-21 de ISO646-DE csISO21German } + { NF_Z_62-010_(1973) iso-ir-25 ISO646-FR1 csISO25French } + { Latin-greek-1 iso-ir-27 csISO27LatinGreek1 } + { ISO_5427 iso-ir-37 csISO5427Cyrillic } + { JIS_C6226-1978 iso-ir-42 csISO42JISC62261978 } + { BS_viewdata iso-ir-47 csISO47BSViewdata } + { INIS iso-ir-49 csISO49INIS } + { INIS-8 iso-ir-50 csISO50INIS8 } + { INIS-cyrillic iso-ir-51 csISO51INISCyrillic } + { ISO_5427:1981 iso-ir-54 ISO5427Cyrillic1981 } + { ISO_5428:1980 iso-ir-55 csISO5428Greek } + { GB_1988-80 iso-ir-57 cn ISO646-CN csISO57GB1988 } + { GB_2312-80 iso-ir-58 chinese csISO58GB231280 } + { NS_4551-1 iso-ir-60 ISO646-NO no csISO60DanishNorwegian + csISO60Norwegian1 } + { NS_4551-2 ISO646-NO2 iso-ir-61 no2 csISO61Norwegian2 } + { NF_Z_62-010 iso-ir-69 ISO646-FR fr csISO69French } + { videotex-suppl iso-ir-70 csISO70VideotexSupp1 } + { PT2 iso-ir-84 ISO646-PT2 csISO84Portuguese2 } + { ES2 iso-ir-85 ISO646-ES2 csISO85Spanish2 } + { MSZ_7795.3 iso-ir-86 ISO646-HU hu csISO86Hungarian } + { JIS_C6226-1983 iso-ir-87 x0208 JIS_X0208-1983 csISO87JISX0208 } + { greek7 iso-ir-88 csISO88Greek7 } + { ASMO_449 ISO_9036 arabic7 iso-ir-89 csISO89ASMO449 } + { iso-ir-90 csISO90 } + { JIS_C6229-1984-a iso-ir-91 jp-ocr-a csISO91JISC62291984a } + { JIS_C6229-1984-b iso-ir-92 ISO646-JP-OCR-B jp-ocr-b + csISO92JISC62991984b } + { JIS_C6229-1984-b-add iso-ir-93 jp-ocr-b-add csISO93JIS62291984badd } + { JIS_C6229-1984-hand iso-ir-94 jp-ocr-hand csISO94JIS62291984hand } + { JIS_C6229-1984-hand-add iso-ir-95 jp-ocr-hand-add + csISO95JIS62291984handadd } + { JIS_C6229-1984-kana iso-ir-96 csISO96JISC62291984kana } + { ISO_2033-1983 iso-ir-98 e13b csISO2033 } + { ANSI_X3.110-1983 iso-ir-99 CSA_T500-1983 NAPLPS csISO99NAPLPS } + { ISO_8859-1:1987 iso-ir-100 ISO_8859-1 ISO-8859-1 latin1 l1 IBM819 + CP819 csISOLatin1 } + { ISO_8859-2:1987 iso-ir-101 ISO_8859-2 ISO-8859-2 latin2 l2 csISOLatin2 } + { T.61-7bit iso-ir-102 csISO102T617bit } + { T.61-8bit T.61 iso-ir-103 csISO103T618bit } + { ISO_8859-3:1988 iso-ir-109 ISO_8859-3 ISO-8859-3 latin3 l3 csISOLatin3 } + { ISO_8859-4:1988 iso-ir-110 ISO_8859-4 ISO-8859-4 latin4 l4 csISOLatin4 } + { ECMA-cyrillic iso-ir-111 KOI8-E csISO111ECMACyrillic } + { CSA_Z243.4-1985-1 iso-ir-121 ISO646-CA csa7-1 ca csISO121Canadian1 } + { CSA_Z243.4-1985-2 iso-ir-122 ISO646-CA2 csa7-2 csISO122Canadian2 } + { CSA_Z243.4-1985-gr iso-ir-123 csISO123CSAZ24341985gr } + { ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ISO-8859-6 ECMA-114 ASMO-708 + arabic csISOLatinArabic } + { ISO_8859-6-E csISO88596E ISO-8859-6-E } + { ISO_8859-6-I csISO88596I ISO-8859-6-I } + { ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ISO-8859-7 ELOT_928 ECMA-118 + greek greek8 csISOLatinGreek } + { T.101-G2 iso-ir-128 csISO128T101G2 } + { ISO_8859-8:1988 iso-ir-138 ISO_8859-8 ISO-8859-8 hebrew + csISOLatinHebrew } + { ISO_8859-8-E csISO88598E ISO-8859-8-E } + { ISO_8859-8-I csISO88598I ISO-8859-8-I } + { CSN_369103 iso-ir-139 csISO139CSN369103 } + { JUS_I.B1.002 iso-ir-141 ISO646-YU js yu csISO141JUSIB1002 } + { ISO_6937-2-add iso-ir-142 csISOTextComm } + { IEC_P27-1 iso-ir-143 csISO143IECP271 } + { ISO_8859-5:1988 iso-ir-144 ISO_8859-5 ISO-8859-5 cyrillic + csISOLatinCyrillic } + { JUS_I.B1.003-serb iso-ir-146 serbian csISO146Serbian } + { JUS_I.B1.003-mac macedonian iso-ir-147 csISO147Macedonian } + { ISO_8859-9:1989 iso-ir-148 ISO_8859-9 ISO-8859-9 latin5 l5 csISOLatin5 } + { greek-ccitt iso-ir-150 csISO150 csISO150GreekCCITT } + { NC_NC00-10:81 cuba iso-ir-151 ISO646-CU csISO151Cuba } + { ISO_6937-2-25 iso-ir-152 csISO6937Add } + { GOST_19768-74 ST_SEV_358-88 iso-ir-153 csISO153GOST1976874 } + { ISO_8859-supp iso-ir-154 latin1-2-5 csISO8859Supp } + { ISO_10367-box iso-ir-155 csISO10367Box } + { ISO-8859-10 iso-ir-157 l6 ISO_8859-10:1992 csISOLatin6 latin6 } + { latin-lap lap iso-ir-158 csISO158Lap } + { JIS_X0212-1990 x0212 iso-ir-159 csISO159JISX02121990 } + { DS_2089 DS2089 ISO646-DK dk csISO646Danish } + { us-dk csUSDK } + { dk-us csDKUS } + { JIS_X0201 X0201 csHalfWidthKatakana } + { KSC5636 ISO646-KR csKSC5636 } + { ISO-10646-UCS-2 csUnicode } + { ISO-10646-UCS-4 csUCS4 } + { DEC-MCS dec csDECMCS } + { hp-roman8 roman8 r8 csHPRoman8 } + { macintosh mac csMacintosh } + { IBM037 cp037 ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl + csIBM037 } + { IBM038 EBCDIC-INT cp038 csIBM038 } + { IBM273 CP273 csIBM273 } + { IBM274 EBCDIC-BE CP274 csIBM274 } + { IBM275 EBCDIC-BR cp275 csIBM275 } + { IBM277 EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 } + { IBM278 CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 } + { IBM280 CP280 ebcdic-cp-it csIBM280 } + { IBM281 EBCDIC-JP-E cp281 csIBM281 } + { IBM284 CP284 ebcdic-cp-es csIBM284 } + { IBM285 CP285 ebcdic-cp-gb csIBM285 } + { IBM290 cp290 EBCDIC-JP-kana csIBM290 } + { IBM297 cp297 ebcdic-cp-fr csIBM297 } + { IBM420 cp420 ebcdic-cp-ar1 csIBM420 } + { IBM423 cp423 ebcdic-cp-gr csIBM423 } + { IBM424 cp424 ebcdic-cp-he csIBM424 } + { IBM437 cp437 437 csPC8CodePage437 } + { IBM500 CP500 ebcdic-cp-be ebcdic-cp-ch csIBM500 } + { IBM775 cp775 csPC775Baltic } + { IBM850 cp850 850 csPC850Multilingual } + { IBM851 cp851 851 csIBM851 } + { IBM852 cp852 852 csPCp852 } + { IBM855 cp855 855 csIBM855 } + { IBM857 cp857 857 csIBM857 } + { IBM860 cp860 860 csIBM860 } + { IBM861 cp861 861 cp-is csIBM861 } + { IBM862 cp862 862 csPC862LatinHebrew } + { IBM863 cp863 863 csIBM863 } + { IBM864 cp864 csIBM864 } + { IBM865 cp865 865 csIBM865 } + { IBM866 cp866 866 csIBM866 } + { IBM868 CP868 cp-ar csIBM868 } + { IBM869 cp869 869 cp-gr csIBM869 } + { IBM870 CP870 ebcdic-cp-roece ebcdic-cp-yu csIBM870 } + { IBM871 CP871 ebcdic-cp-is csIBM871 } + { IBM880 cp880 EBCDIC-Cyrillic csIBM880 } + { IBM891 cp891 csIBM891 } + { IBM903 cp903 csIBM903 } + { IBM904 cp904 904 csIBBM904 } + { IBM905 CP905 ebcdic-cp-tr csIBM905 } + { IBM918 CP918 ebcdic-cp-ar2 csIBM918 } + { IBM1026 CP1026 csIBM1026 } + { EBCDIC-AT-DE csIBMEBCDICATDE } + { EBCDIC-AT-DE-A csEBCDICATDEA } + { EBCDIC-CA-FR csEBCDICCAFR } + { EBCDIC-DK-NO csEBCDICDKNO } + { EBCDIC-DK-NO-A csEBCDICDKNOA } + { EBCDIC-FI-SE csEBCDICFISE } + { EBCDIC-FI-SE-A csEBCDICFISEA } + { EBCDIC-FR csEBCDICFR } + { EBCDIC-IT csEBCDICIT } + { EBCDIC-PT csEBCDICPT } + { EBCDIC-ES csEBCDICES } + { EBCDIC-ES-A csEBCDICESA } + { EBCDIC-ES-S csEBCDICESS } + { EBCDIC-UK csEBCDICUK } + { EBCDIC-US csEBCDICUS } + { UNKNOWN-8BIT csUnknown8BiT } + { MNEMONIC csMnemonic } + { MNEM csMnem } + { VISCII csVISCII } + { VIQR csVIQR } + { KOI8-R csKOI8R } + { IBM00858 CCSID00858 CP00858 PC-Multilingual-850+euro } + { IBM00924 CCSID00924 CP00924 ebcdic-Latin9--euro } + { IBM01140 CCSID01140 CP01140 ebcdic-us-37+euro } + { IBM01141 CCSID01141 CP01141 ebcdic-de-273+euro } + { IBM01142 CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro } + { IBM01143 CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro } + { IBM01144 CCSID01144 CP01144 ebcdic-it-280+euro } + { IBM01145 CCSID01145 CP01145 ebcdic-es-284+euro } + { IBM01146 CCSID01146 CP01146 ebcdic-gb-285+euro } + { IBM01147 CCSID01147 CP01147 ebcdic-fr-297+euro } + { IBM01148 CCSID01148 CP01148 ebcdic-international-500+euro } + { IBM01149 CCSID01149 CP01149 ebcdic-is-871+euro } + { IBM1047 IBM-1047 } + { PTCP154 csPTCP154 PT154 CP154 Cyrillic-Asian } + { Amiga-1251 Ami1251 Amiga1251 Ami-1251 } + { UNICODE-1-1 csUnicode11 } + { CESU-8 csCESU-8 } + { BOCU-1 csBOCU-1 } + { UNICODE-1-1-UTF-7 csUnicode11UTF7 } + { ISO-8859-14 iso-ir-199 ISO_8859-14:1998 ISO_8859-14 latin8 iso-celtic + l8 } + { ISO-8859-15 ISO_8859-15 Latin-9 } + { ISO-8859-16 iso-ir-226 ISO_8859-16:2001 ISO_8859-16 latin10 l10 } + { GBK CP936 MS936 windows-936 } + { JIS_Encoding csJISEncoding } + { Shift_JIS MS_Kanji csShiftJIS ShiftJIS Shift-JIS } + { Extended_UNIX_Code_Packed_Format_for_Japanese csEUCPkdFmtJapanese + EUC-JP } + { Extended_UNIX_Code_Fixed_Width_for_Japanese csEUCFixWidJapanese } + { ISO-10646-UCS-Basic csUnicodeASCII } + { ISO-10646-Unicode-Latin1 csUnicodeLatin1 ISO-10646 } + { ISO-Unicode-IBM-1261 csUnicodeIBM1261 } + { ISO-Unicode-IBM-1268 csUnicodeIBM1268 } + { ISO-Unicode-IBM-1276 csUnicodeIBM1276 } + { ISO-Unicode-IBM-1264 csUnicodeIBM1264 } + { ISO-Unicode-IBM-1265 csUnicodeIBM1265 } + { ISO-8859-1-Windows-3.0-Latin-1 csWindows30Latin1 } + { ISO-8859-1-Windows-3.1-Latin-1 csWindows31Latin1 } + { ISO-8859-2-Windows-Latin-2 csWindows31Latin2 } + { ISO-8859-9-Windows-Latin-5 csWindows31Latin5 } + { Adobe-Standard-Encoding csAdobeStandardEncoding } + { Ventura-US csVenturaUS } + { Ventura-International csVenturaInternational } + { PC8-Danish-Norwegian csPC8DanishNorwegian } + { PC8-Turkish csPC8Turkish } + { IBM-Symbols csIBMSymbols } + { IBM-Thai csIBMThai } + { HP-Legal csHPLegal } + { HP-Pi-font csHPPiFont } + { HP-Math8 csHPMath8 } + { Adobe-Symbol-Encoding csHPPSMath } + { HP-DeskTop csHPDesktop } + { Ventura-Math csVenturaMath } + { Microsoft-Publishing csMicrosoftPublishing } + { Windows-31J csWindows31J } + { GB2312 csGB2312 } + { Big5 csBig5 } +} + +set encoding_groups { + {"" "" + {"Unicode" UTF-8} + {"Western" ISO-8859-1}} + {we "West European" + {"Western" ISO-8859-15 CP-437 CP-850 MacRoman CP-1252 Windows-1252} + {"Celtic" ISO-8859-14} + {"Greek" ISO-8859-14 ISO-8859-7 CP-737 CP-869 MacGreek CP-1253 Windows-1253} + {"Icelandic" MacIceland MacIcelandic CP-861} + {"Nordic" ISO-8859-10 CP-865} + {"Portuguese" CP-860} + {"South European" ISO-8859-3}} + {ee "East European" + {"Baltic" CP-775 ISO-8859-4 ISO-8859-13 CP-1257 Windows-1257} + {"Central European" CP-852 ISO-8859-2 MacCE CP-1250 Windows-1250} + {"Croatian" MacCroatian} + {"Cyrillic" CP-855 ISO-8859-5 ISO-IR-111 KOI8-R MacCyrillic CP-1251 Windows-1251} + {"Russian" CP-866} + {"Ukrainian" KOI8-U MacUkraine MacUkrainian} + {"Romanian" ISO-8859-16 MacRomania MacRomanian}} + {ea "East Asian" + {"Generic" ISO-2022} + {"Chinese Simplified" GB2312 GB1988 GB12345 GB2312-RAW GBK EUC-CN GB18030 HZ ISO-2022-CN} + {"Chinese Traditional" Big5 Big5-HKSCS EUC-TW CP-950} + {"Japanese" EUC-JP ISO-2022-JP Shift-JIS JIS-0212 JIS-0208 JIS-0201 CP-932 MacJapan} + {"Korean" EUC-KR UHC JOHAB ISO-2022-KR CP-949 KSC5601}} + {sa "SE & SW Asian" + {"Armenian" ARMSCII-8} + {"Georgian" GEOSTD8} + {"Thai" TIS-620 ISO-8859-11 CP-874 Windows-874 MacThai} + {"Turkish" CP-857 CP857 ISO-8859-9 MacTurkish CP-1254 Windows-1254} + {"Vietnamese" TCVN VISCII VPS CP-1258 Windows-1258} + {"Hindi" MacDevanagari} + {"Gujarati" MacGujarati} + {"Gurmukhi" MacGurmukhi}} + {me "Middle Eastern" + {"Arabic" ISO-8859-6 Windows-1256 CP-1256 CP-864 MacArabic} + {"Farsi" MacFarsi} + {"Hebrew" ISO-8859-8-I Windows-1255 CP-1255 ISO-8859-8 CP-862 MacHebrew}} + {mi "Misc" + {"7-bit" ASCII} + {"16-bit" Unicode} + {"Legacy" CP-863 EBCDIC} + {"Symbol" Symbol Dingbats MacDingbats MacCentEuro}} +} + +proc build_encoding_table {} { + global encoding_aliases encoding_lookup_table + + # Prepare the lookup list; cannot use lsort -nocase because + # of compatibility issues with older Tcl (e.g. in msysgit) + set names [list] + foreach item [encoding names] { + lappend names [list [string tolower $item] $item] + } + set names [lsort -ascii -index 0 $names] + # neither can we use lsearch -index + set lnames [list] + foreach item $names { + lappend lnames [lindex $item 0] + } + + foreach grp $encoding_aliases { + set target {} + foreach item $grp { + set i [lsearch -sorted -ascii $lnames \ + [string tolower $item]] + if {$i >= 0} { + set target [lindex $names $i 1] + break + } + } + if {$target eq {}} continue + foreach item $grp { + set encoding_lookup_table([string tolower $item]) $target + } + } + + foreach item $names { + set encoding_lookup_table([lindex $item 0]) [lindex $item 1] + } +} + +proc tcl_encoding {enc} { + global encoding_lookup_table + if {$enc eq {}} { + return {} + } + if {![info exists encoding_lookup_table]} { + build_encoding_table + } + set enc [string tolower $enc] + if {![info exists encoding_lookup_table($enc)]} { + # look for "isonnn" instead of "iso-nnn" or "iso_nnn" + if {[regsub {^(iso|cp|ibm|jis)[-_]} $enc {\1} encx]} { + set enc $encx + } + } + if {[info exists encoding_lookup_table($enc)]} { + return $encoding_lookup_table($enc) + } else { + return {} + } +} + +proc force_path_encoding {path enc} { + global path_encoding_overrides last_encoding_override + + set enc [tcl_encoding $enc] + if {$enc eq {}} { + catch { unset last_encoding_override } + catch { unset path_encoding_overrides($path) } + } else { + set last_encoding_override $enc + if {$path ne {}} { + set path_encoding_overrides($path) $enc + } + } +} + +proc get_path_encoding {path} { + global path_encoding_overrides last_encoding_override + + if {[info exists last_encoding_override]} { + set tcl_enc $last_encoding_override + } else { + set tcl_enc [tcl_encoding [get_config gui.encoding]] + } + if {$tcl_enc eq {}} { + set tcl_enc [encoding system] + } + if {$path ne {}} { + if {[info exists path_encoding_overrides($path)]} { + set enc2 $path_encoding_overrides($path) + } else { + set enc2 [tcl_encoding [gitattr $path encoding $tcl_enc]] + } + if {$enc2 ne {}} { + set tcl_enc $enc2 + } + } + return $tcl_enc +} + +proc build_encoding_submenu {parent grp cmd} { + global used_encodings + + set mid [lindex $grp 0] + set gname [mc [lindex $grp 1]] + + set smenu {} + foreach subset [lrange $grp 2 end] { + set name [mc [lindex $subset 0]] + + foreach enc [lrange $subset 1 end] { + set tcl_enc [tcl_encoding $enc] + if {$tcl_enc eq {}} continue + + if {$smenu eq {}} { + if {$mid eq {}} { + set smenu $parent + } else { + set smenu "$parent.$mid" + menu $smenu + $parent add cascade \ + -label $gname \ + -menu $smenu + } + } + + if {$name ne {}} { + set lbl "$name ($enc)" + } else { + set lbl $enc + } + $smenu add command \ + -label $lbl \ + -command [concat $cmd [list $tcl_enc]] + + lappend used_encodings $tcl_enc + } + } +} + +proc popup_btn_menu {m b} { + tk_popup $m [winfo pointerx $b] [winfo pointery $b] +} + +proc build_encoding_menu {emenu cmd {nodef 0}} { + $emenu configure -postcommand \ + [list do_build_encoding_menu $emenu $cmd $nodef] +} + +proc do_build_encoding_menu {emenu cmd {nodef 0}} { + global used_encodings encoding_groups + + $emenu configure -postcommand {} + + if {!$nodef} { + $emenu add command \ + -label [mc "Default"] \ + -command [concat $cmd [list {}]] + } + set sysenc [encoding system] + $emenu add command \ + -label [mc "System (%s)" $sysenc] \ + -command [concat $cmd [list $sysenc]] + + # Main encoding tree + set used_encodings [list identity] + $emenu add separator + foreach grp $encoding_groups { + build_encoding_submenu $emenu $grp $cmd + } + + # Add unclassified encodings + set unused_grp [list [mc Other]] + foreach enc [encoding names] { + if {[lsearch -exact $used_encodings $enc] < 0} { + lappend unused_grp $enc + } + } + build_encoding_submenu $emenu [list other [mc Other] $unused_grp] $cmd +} diff --git a/git-gui/lib/error.tcl b/git-gui/lib/error.tcl new file mode 100644 index 0000000..8968a57 --- /dev/null +++ b/git-gui/lib/error.tcl @@ -0,0 +1,119 @@ +# git-gui branch (create/delete) support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc _error_parent {} { + set p [grab current .] + if {$p eq {}} { + return . + } + return $p +} + +proc error_popup {msg} { + set title [appname] + if {[reponame] ne {}} { + append title " ([reponame])" + } + set cmd [list tk_messageBox \ + -icon error \ + -type ok \ + -title [mc "%s: error" $title] \ + -message $msg] + if {[winfo ismapped [_error_parent]]} { + lappend cmd -parent [_error_parent] + } + eval $cmd +} + +proc warn_popup {msg} { + set title [appname] + if {[reponame] ne {}} { + append title " ([reponame])" + } + set cmd [list tk_messageBox \ + -icon warning \ + -type ok \ + -title [mc "%s: warning" $title] \ + -message $msg] + if {[winfo ismapped [_error_parent]]} { + lappend cmd -parent [_error_parent] + } + eval $cmd +} + +proc info_popup {msg} { + set title [appname] + if {[reponame] ne {}} { + append title " ([reponame])" + } + tk_messageBox \ + -parent [_error_parent] \ + -icon info \ + -type ok \ + -title $title \ + -message $msg +} + +proc ask_popup {msg} { + set title [appname] + if {[reponame] ne {}} { + append title " ([reponame])" + } + set cmd [list tk_messageBox \ + -icon question \ + -type yesno \ + -title $title \ + -message $msg] + if {[winfo ismapped [_error_parent]]} { + lappend cmd -parent [_error_parent] + } + eval $cmd +} + +proc hook_failed_popup {hook msg {is_fatal 1}} { + global use_ttk NS + set w .hookfail + Dialog $w + wm withdraw $w + + ${NS}::frame $w.m + ${NS}::label $w.m.l1 -text [mc "%s hook failed:" $hook] \ + -anchor w \ + -justify left \ + -font font_uibold + text $w.m.t \ + -background white \ + -foreground black \ + -borderwidth 1 \ + -relief sunken \ + -width 80 -height 10 \ + -font font_diff \ + -yscrollcommand [list $w.m.sby set] + ${NS}::scrollbar $w.m.sby -command [list $w.m.t yview] + pack $w.m.l1 -side top -fill x + if {$is_fatal} { + ${NS}::label $w.m.l2 \ + -text [mc "You must correct the above errors before committing."] \ + -anchor w \ + -justify left \ + -font font_uibold + pack $w.m.l2 -side bottom -fill x + } + pack $w.m.sby -side right -fill y + pack $w.m.t -side left -fill both -expand 1 + pack $w.m -side top -fill both -expand 1 -padx 5 -pady 10 + + $w.m.t insert 1.0 $msg + $w.m.t conf -state disabled + + ${NS}::button $w.ok -text OK \ + -width 15 \ + -command "destroy $w" + pack $w.ok -side bottom -anchor e -pady 10 -padx 10 + + bind $w <Visibility> "grab $w; focus $w" + bind $w <Key-Return> "destroy $w" + wm title $w [mc "%s (%s): error" [appname] [reponame]] + wm deiconify $w + tkwait window $w +} diff --git a/git-gui/lib/git-gui.ico b/git-gui/lib/git-gui.ico Binary files differnew file mode 100644 index 0000000..334cfa5 --- /dev/null +++ b/git-gui/lib/git-gui.ico diff --git a/git-gui/lib/index.tcl b/git-gui/lib/index.tcl new file mode 100644 index 0000000..1fc5b42 --- /dev/null +++ b/git-gui/lib/index.tcl @@ -0,0 +1,753 @@ +# git-gui index (add/remove) support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc _delete_indexlock {} { + if {[catch {file delete -- [gitdir index.lock]} err]} { + error_popup [strcat [mc "Unable to unlock the index."] "\n\n$err"] + } +} + +proc close_and_unlock_index {fd after} { + if {![catch {_close_updateindex $fd} err]} { + unlock_index + uplevel #0 $after + } else { + rescan_on_error $err $after + } +} + +proc _close_updateindex {fd} { + fconfigure $fd -blocking 1 + close $fd +} + +proc rescan_on_error {err {after {}}} { + global use_ttk NS + + set w .indexfried + Dialog $w + wm withdraw $w + wm title $w [strcat "[appname] ([reponame]): " [mc "Index Error"]] + wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + set s [mc "Updating the Git index failed. A rescan will be automatically started to resynchronize git-gui."] + text $w.msg -yscrollcommand [list $w.vs set] \ + -width [string length $s] -relief flat \ + -borderwidth 0 -highlightthickness 0 \ + -background [get_bg_color $w] + $w.msg tag configure bold -font font_uibold -justify center + ${NS}::scrollbar $w.vs -command [list $w.msg yview] + $w.msg insert end $s bold \n\n$err {} + $w.msg configure -state disabled + + ${NS}::button $w.continue \ + -text [mc "Continue"] \ + -command [list destroy $w] + ${NS}::button $w.unlock \ + -text [mc "Unlock Index"] \ + -command "destroy $w; _delete_indexlock" + grid $w.msg - $w.vs -sticky news + grid $w.unlock $w.continue - -sticky se -padx 2 -pady 2 + grid columnconfigure $w 0 -weight 1 + grid rowconfigure $w 0 -weight 1 + + wm protocol $w WM_DELETE_WINDOW update + bind $w.continue <Visibility> " + grab $w + focus %W + " + wm deiconify $w + tkwait window $w + + $::main_status stop_all + unlock_index + rescan [concat $after [list ui_ready]] 0 +} + +proc update_indexinfo {msg path_list after} { + global update_index_cp + + if {![lock_index update]} return + + set update_index_cp 0 + set path_list [lsort $path_list] + set total_cnt [llength $path_list] + set batch [expr {int($total_cnt * .01) + 1}] + if {$batch > 25} {set batch 25} + + set status_bar_operation [$::main_status start $msg [mc "files"]] + set fd [git_write update-index -z --index-info] + fconfigure $fd \ + -blocking 0 \ + -buffering full \ + -buffersize 512 \ + -encoding binary \ + -translation binary + fileevent $fd writable [list \ + write_update_indexinfo \ + $fd \ + $path_list \ + $total_cnt \ + $batch \ + $status_bar_operation \ + $after \ + ] +} + +proc write_update_indexinfo {fd path_list total_cnt batch status_bar_operation \ + after} { + global update_index_cp + global file_states current_diff_path + + if {$update_index_cp >= $total_cnt} { + $status_bar_operation stop + close_and_unlock_index $fd $after + return + } + + for {set i $batch} \ + {$update_index_cp < $total_cnt && $i > 0} \ + {incr i -1} { + set path [lindex $path_list $update_index_cp] + incr update_index_cp + + set s $file_states($path) + switch -glob -- [lindex $s 0] { + A? {set new _O} + MT - + TM - + T_ {set new _T} + M? {set new _M} + TD - + D_ {set new _D} + D? {set new _?} + ?? {continue} + } + set info [lindex $s 2] + if {$info eq {}} continue + + puts -nonewline $fd "$info\t[encoding convertto utf-8 $path]\0" + display_file $path $new + } + + $status_bar_operation update $update_index_cp $total_cnt +} + +proc update_index {msg path_list after} { + global update_index_cp + + if {![lock_index update]} return + + set update_index_cp 0 + set path_list [lsort $path_list] + set total_cnt [llength $path_list] + set batch [expr {int($total_cnt * .01) + 1}] + if {$batch > 25} {set batch 25} + + set status_bar_operation [$::main_status start $msg [mc "files"]] + set fd [git_write update-index --add --remove -z --stdin] + fconfigure $fd \ + -blocking 0 \ + -buffering full \ + -buffersize 512 \ + -encoding binary \ + -translation binary + fileevent $fd writable [list \ + write_update_index \ + $fd \ + $path_list \ + $total_cnt \ + $batch \ + $status_bar_operation \ + $after \ + ] +} + +proc write_update_index {fd path_list total_cnt batch status_bar_operation \ + after} { + global update_index_cp + global file_states current_diff_path + + if {$update_index_cp >= $total_cnt} { + $status_bar_operation stop + close_and_unlock_index $fd $after + return + } + + for {set i $batch} \ + {$update_index_cp < $total_cnt && $i > 0} \ + {incr i -1} { + set path [lindex $path_list $update_index_cp] + incr update_index_cp + + switch -glob -- [lindex $file_states($path) 0] { + AD {set new __} + ?D {set new D_} + _O - + AT - + AM {set new A_} + TM - + MT - + _T {set new T_} + _U - + U? { + if {[file exists $path]} { + set new M_ + } else { + set new D_ + } + } + ?M {set new M_} + ?? {continue} + } + puts -nonewline $fd "[encoding convertto utf-8 $path]\0" + display_file $path $new + } + + $status_bar_operation update $update_index_cp $total_cnt +} + +proc checkout_index {msg path_list after capture_error} { + global update_index_cp + + if {![lock_index update]} return + + set update_index_cp 0 + set path_list [lsort $path_list] + set total_cnt [llength $path_list] + set batch [expr {int($total_cnt * .01) + 1}] + if {$batch > 25} {set batch 25} + + set status_bar_operation [$::main_status start $msg [mc "files"]] + set fd [git_write checkout-index \ + --index \ + --quiet \ + --force \ + -z \ + --stdin \ + ] + fconfigure $fd \ + -blocking 0 \ + -buffering full \ + -buffersize 512 \ + -encoding binary \ + -translation binary + fileevent $fd writable [list \ + write_checkout_index \ + $fd \ + $path_list \ + $total_cnt \ + $batch \ + $status_bar_operation \ + $after \ + $capture_error \ + ] +} + +proc write_checkout_index {fd path_list total_cnt batch status_bar_operation \ + after capture_error} { + global update_index_cp + global file_states current_diff_path + + if {$update_index_cp >= $total_cnt} { + $status_bar_operation stop + + # We do not unlock the index directly here because this + # operation expects to potentially run in parallel with file + # deletions scheduled by revert_helper. We're done with the + # update index, so we close it, but actually unlocking the index + # and dealing with potential errors is deferred to the chord + # body that runs when all async operations are completed. + # + # (See after_chord in revert_helper.) + + if {[catch {_close_updateindex $fd} err]} { + uplevel #0 $capture_error [list $err] + } + + uplevel #0 $after + + return + } + + for {set i $batch} \ + {$update_index_cp < $total_cnt && $i > 0} \ + {incr i -1} { + set path [lindex $path_list $update_index_cp] + incr update_index_cp + switch -glob -- [lindex $file_states($path) 0] { + U? {continue} + ?M - + ?T - + ?D { + puts -nonewline $fd "[encoding convertto utf-8 $path]\0" + display_file $path ?_ + } + } + } + + $status_bar_operation update $update_index_cp $total_cnt +} + +proc unstage_helper {txt paths} { + global file_states current_diff_path + + if {![lock_index begin-update]} return + + set path_list [list] + set after {} + foreach path $paths { + switch -glob -- [lindex $file_states($path) 0] { + A? - + M? - + T? - + D? { + lappend path_list $path + if {$path eq $current_diff_path} { + set after {reshow_diff;} + } + } + } + } + if {$path_list eq {}} { + unlock_index + } else { + update_indexinfo \ + $txt \ + $path_list \ + [concat $after [list ui_ready]] + } +} + +proc do_unstage_selection {} { + global current_diff_path selected_paths + + if {[array size selected_paths] > 0} { + unstage_helper \ + [mc "Unstaging selected files from commit"] \ + [array names selected_paths] + } elseif {$current_diff_path ne {}} { + unstage_helper \ + [mc "Unstaging %s from commit" [short_path $current_diff_path]] \ + [list $current_diff_path] + } +} + +proc add_helper {txt paths} { + global file_states current_diff_path + + if {![lock_index begin-update]} return + + set path_list [list] + set after {} + foreach path $paths { + switch -glob -- [lindex $file_states($path) 0] { + _U - + U? { + if {$path eq $current_diff_path} { + unlock_index + merge_stage_workdir $path + return + } + } + _O - + ?M - + ?D - + ?T { + lappend path_list $path + if {$path eq $current_diff_path} { + set after {reshow_diff;} + } + } + } + } + if {$path_list eq {}} { + unlock_index + } else { + update_index \ + $txt \ + $path_list \ + [concat $after {ui_status [mc "Ready to commit."]}] + } +} + +proc do_add_selection {} { + global current_diff_path selected_paths + + if {[array size selected_paths] > 0} { + add_helper \ + [mc "Adding selected files"] \ + [array names selected_paths] + } elseif {$current_diff_path ne {}} { + add_helper \ + [mc "Adding %s" [short_path $current_diff_path]] \ + [list $current_diff_path] + } +} + +proc do_add_all {} { + global file_states + + set paths [list] + set untracked_paths [list] + foreach path [array names file_states] { + switch -glob -- [lindex $file_states($path) 0] { + U? {continue} + ?M - + ?T - + ?D {lappend paths $path} + ?O {lappend untracked_paths $path} + } + } + if {[llength $untracked_paths]} { + set reply 0 + switch -- [get_config gui.stageuntracked] { + no { + set reply 0 + } + yes { + set reply 1 + } + ask - + default { + set reply [ask_popup [mc "Stage %d untracked files?" \ + [llength $untracked_paths]]] + } + } + if {$reply} { + set paths [concat $paths $untracked_paths] + } + } + add_helper [mc "Adding all changed files"] $paths +} + +# Copied from TclLib package "lambda". +proc lambda {arguments body args} { + return [list ::apply [list $arguments $body] {*}$args] +} + +proc revert_helper {txt paths} { + global file_states current_diff_path + + if {![lock_index begin-update]} return + + # Common "after" functionality that waits until multiple asynchronous + # operations are complete (by waiting for them to activate their notes + # on the chord). + # + # The asynchronous operations are each indicated below by a comment + # before the code block that starts the async operation. + set after_chord [SimpleChord::new { + if {[string trim $err] != ""} { + rescan_on_error $err + } else { + unlock_index + if {$should_reshow_diff} { reshow_diff } + ui_ready + } + }] + + $after_chord eval { set should_reshow_diff 0 } + + # This function captures an error for processing when after_chord is + # completed. (The chord is curried into the lambda function.) + set capture_error [lambda \ + {chord error} \ + { $chord eval [list set err $error] } \ + $after_chord] + + # We don't know how many notes we're going to create (it's dynamic based + # on conditional paths below), so create a common note that will delay + # the chord's completion until we activate it, and then activate it + # after all the other notes have been created. + set after_common_note [$after_chord add_note] + + set path_list [list] + set untracked_list [list] + + foreach path $paths { + switch -glob -- [lindex $file_states($path) 0] { + U? {continue} + ?O { + lappend untracked_list $path + } + ?M - + ?T - + ?D { + lappend path_list $path + if {$path eq $current_diff_path} { + $after_chord eval { set should_reshow_diff 1 } + } + } + } + } + + set path_cnt [llength $path_list] + set untracked_cnt [llength $untracked_list] + + # Asynchronous operation: revert changes by checking them out afresh + # from the index. + if {$path_cnt > 0} { + # Split question between singular and plural cases, because + # such distinction is needed in some languages. Previously, the + # code used "Revert changes in" for both, but that can't work + # in languages where 'in' must be combined with word from + # rest of string (in different way for both cases of course). + # + # FIXME: Unfortunately, even that isn't enough in some languages + # as they have quite complex plural-form rules. Unfortunately, + # msgcat doesn't seem to support that kind of string + # translation. + # + if {$path_cnt == 1} { + set query [mc \ + "Revert changes in file %s?" \ + [short_path [lindex $path_list]] \ + ] + } else { + set query [mc \ + "Revert changes in these %i files?" \ + $path_cnt] + } + + set reply [tk_dialog \ + .confirm_revert \ + "[appname] ([reponame])" \ + "$query + +[mc "Any unstaged changes will be permanently lost by the revert."]" \ + question \ + 1 \ + [mc "Do Nothing"] \ + [mc "Revert Changes"] \ + ] + + if {$reply == 1} { + set note [$after_chord add_note] + checkout_index \ + $txt \ + $path_list \ + [list $note activate] \ + $capture_error + } + } + + # Asynchronous operation: Deletion of untracked files. + if {$untracked_cnt > 0} { + # Split question between singular and plural cases, because + # such distinction is needed in some languages. + # + # FIXME: Unfortunately, even that isn't enough in some languages + # as they have quite complex plural-form rules. Unfortunately, + # msgcat doesn't seem to support that kind of string + # translation. + # + if {$untracked_cnt == 1} { + set query [mc \ + "Delete untracked file %s?" \ + [short_path [lindex $untracked_list]] \ + ] + } else { + set query [mc \ + "Delete these %i untracked files?" \ + $untracked_cnt \ + ] + } + + set reply [tk_dialog \ + .confirm_revert \ + "[appname] ([reponame])" \ + "$query + +[mc "Files will be permanently deleted."]" \ + question \ + 1 \ + [mc "Do Nothing"] \ + [mc "Delete Files"] \ + ] + + if {$reply == 1} { + $after_chord eval { set should_reshow_diff 1 } + + set note [$after_chord add_note] + delete_files $untracked_list [list $note activate] + } + } + + # Activate the common note. If no other notes were created, this + # completes the chord. If other notes were created, then this common + # note prevents a race condition where the chord might complete early. + $after_common_note activate +} + +# Delete all of the specified files, performing deletion in batches to allow the +# UI to remain responsive and updated. +proc delete_files {path_list after} { + # Enable progress bar status updates + set status_bar_operation [$::main_status \ + start \ + [mc "Deleting"] \ + [mc "files"]] + + set path_index 0 + set deletion_errors [list] + set batch_size 50 + + delete_helper \ + $path_list \ + $path_index \ + $deletion_errors \ + $batch_size \ + $status_bar_operation \ + $after +} + +# Helper function to delete a list of files in batches. Each call deletes one +# batch of files, and then schedules a call for the next batch after any UI +# messages have been processed. +proc delete_helper {path_list path_index deletion_errors batch_size \ + status_bar_operation after} { + global file_states + + set path_cnt [llength $path_list] + + set batch_remaining $batch_size + + while {$batch_remaining > 0} { + if {$path_index >= $path_cnt} { break } + + set path [lindex $path_list $path_index] + + set deletion_failed [catch {file delete -- $path} deletion_error] + + if {$deletion_failed} { + lappend deletion_errors [list "$deletion_error"] + } else { + remove_empty_directories [file dirname $path] + + # Don't assume the deletion worked. Remove the file from + # the UI, but only if it no longer exists. + if {![path_exists $path]} { + unset file_states($path) + display_file $path __ + } + } + + incr path_index 1 + incr batch_remaining -1 + } + + # Update the progress bar to indicate that this batch has been + # completed. The update will be visible when this procedure returns + # and allows the UI thread to process messages. + $status_bar_operation update $path_index $path_cnt + + if {$path_index < $path_cnt} { + # The Tcler's Wiki lists this as the best practice for keeping + # a UI active and processing messages during a long-running + # operation. + + after idle [list after 0 [list \ + delete_helper \ + $path_list \ + $path_index \ + $deletion_errors \ + $batch_size \ + $status_bar_operation \ + $after + ]] + } else { + # Finish the status bar operation. + $status_bar_operation stop + + # Report error, if any, based on how many deletions failed. + set deletion_error_cnt [llength $deletion_errors] + + if {($deletion_error_cnt > 0) + && ($deletion_error_cnt <= [MAX_VERBOSE_FILES_IN_DELETION_ERROR])} { + set error_text [mc "Encountered errors deleting files:\n"] + + foreach deletion_error $deletion_errors { + append error_text "* [lindex $deletion_error 0]\n" + } + + error_popup $error_text + } elseif {$deletion_error_cnt == $path_cnt} { + error_popup [mc \ + "None of the %d selected files could be deleted." \ + $path_cnt \ + ] + } elseif {$deletion_error_cnt > 1} { + error_popup [mc \ + "%d of the %d selected files could not be deleted." \ + $deletion_error_cnt \ + $path_cnt \ + ] + } + + uplevel #0 $after + } +} + +proc MAX_VERBOSE_FILES_IN_DELETION_ERROR {} { return 10; } + +# This function is from the TCL documentation: +# +# https://wiki.tcl-lang.org/page/file+exists +# +# [file exists] returns false if the path does exist but is a symlink to a path +# that doesn't exist. This proc returns true if the path exists, regardless of +# whether it is a symlink and whether it is broken. +proc path_exists {name} { + expr {![catch {file lstat $name finfo}]} +} + +# Remove as many empty directories as we can starting at the specified path, +# walking up the directory tree. If we encounter a directory that is not +# empty, or if a directory deletion fails, then we stop the operation and +# return to the caller. Even if this procedure fails to delete any +# directories at all, it does not report failure. +proc remove_empty_directories {directory_path} { + set parent_path [file dirname $directory_path] + + while {$parent_path != $directory_path} { + set contents [glob -nocomplain -dir $directory_path *] + + if {[llength $contents] > 0} { break } + if {[catch {file delete -- $directory_path}]} { break } + + set directory_path $parent_path + set parent_path [file dirname $directory_path] + } +} + +proc do_revert_selection {} { + global current_diff_path selected_paths + + if {[array size selected_paths] > 0} { + revert_helper \ + [mc "Reverting selected files"] \ + [array names selected_paths] + } elseif {$current_diff_path ne {}} { + revert_helper \ + [mc "Reverting %s" [short_path $current_diff_path]] \ + [list $current_diff_path] + } +} + +proc do_select_commit_type {} { + global commit_type commit_type_is_amend + + if {$commit_type_is_amend == 0 + && [string match amend* $commit_type]} { + create_new_commit + } elseif {$commit_type_is_amend == 1 + && ![string match amend* $commit_type]} { + load_last_commit + + # The amend request was rejected... + # + if {![string match amend* $commit_type]} { + set commit_type_is_amend 0 + } + } +} diff --git a/git-gui/lib/line.tcl b/git-gui/lib/line.tcl new file mode 100644 index 0000000..a026de9 --- /dev/null +++ b/git-gui/lib/line.tcl @@ -0,0 +1,81 @@ +# goto line number +# based on code from gitk, Copyright (C) Paul Mackerras + +class linebar { + +field w +field ctext + +field linenum {} + +constructor new {i_w i_text args} { + global use_ttk NS + set w $i_w + set ctext $i_text + + ${NS}::frame $w + ${NS}::label $w.l -text [mc "Goto Line:"] + tentry $w.ent \ + -textvariable ${__this}::linenum \ + -background lightgreen \ + -validate key \ + -validatecommand [cb _validate %P] + ${NS}::button $w.bn -text [mc Go] -command [cb _goto] + + pack $w.l -side left + pack $w.bn -side right + pack $w.ent -side left -expand 1 -fill x + + eval grid conf $w -sticky we $args + grid remove $w + + trace add variable linenum write [cb _goto_cb] + bind $w.ent <Return> [cb _goto] + bind $w.ent <Escape> [cb hide] + + bind $w <Destroy> [list delete_this $this] + return $this +} + +method show {} { + if {![visible $this]} { + grid $w + } + focus -force $w.ent +} + +method hide {} { + if {[visible $this]} { + $w.ent delete 0 end + focus $ctext + grid remove $w + } +} + +method visible {} { + return [winfo ismapped $w] +} + +method editor {} { + return $w.ent +} + +method _validate {P} { + # only accept numbers as input + string is integer $P +} + +method _goto_cb {name ix op} { + after idle [cb _goto 1] +} + +method _goto {{nohide {0}}} { + if {$linenum ne {}} { + $ctext see $linenum.0 + if {!$nohide} { + hide $this + } + } +} + +} diff --git a/git-gui/lib/logo.tcl b/git-gui/lib/logo.tcl new file mode 100644 index 0000000..5ff7669 --- /dev/null +++ b/git-gui/lib/logo.tcl @@ -0,0 +1,43 @@ +# git-gui Git Gui logo +# Copyright (C) 2007 Shawn Pearce + +# Henrik Nyh's alternative Git logo, from his blog post +# http://henrik.nyh.se/2007/06/alternative-git-logo-and-favicon +# +image create photo ::git_logo_data -data { +R0lGODdhYQC8AIQbAGZmZtg4LW9vb3l5eYKCgoyMjEC/TOJpYZWVlZ+fn2/PeKmpqbKysry8vMXF +xZ/fpc/Pz7fnvPXNytnZ2eLi4s/v0vja1+zs7Of36fX19f3z8v///////////////////ywAAAAA +YQC8AAAF/uAmjmRpnmiqrmzrvq4hz3RtGw+s7zx5/7dcb0hUAY8zYXHJRCKVzGjPeYRKry8q0Irt +GrVBr3gFDo/PprKNix6ra+y2902Ly7H05L2dl9n3UX04gGeCf4RFhohiiotdjY5XkJGBfYeUOpOY +iZablXmXURgPpKWmp6ipqYIKqq6vqREjFYK1trUKs7e7vFq5IrS9wsM0vxvBxMm8xsjKzqy6z9J5 +zNPWatXX2k7Z29433d/iMuHj3+Xm2+jp1+vs0+7vz/HyyvT1xPf4wvr7y9H+pBkbBasgLFYGE8ba +o8nTlE4OOYGKKJFOKIopGmLMAnHjDo0eWYAM+WUiSRgj/k+eSKmyBMuWI17C3CATZs2WN1XmPLmT +ZM+QPz0G3VihqNGjSJNWwDCzqdOnUKPu0SChqtWrWLNq3cq1q9evYCVYGCEhgNmzaNOqXcu2rdu3 +cOMGOEBWrt27ePPCpSuirN6/gAO35bvBr+DDiPMSNpy4sWO2ix9Lnmw2MuXLiS1j3gxYM+fPdz2D +Hv1WNOnTak2jXj23LuvXlV3DZq16Nujatjnjzo15N2/Kvn9LDi7cMfHimaUqX868ufPn0KPPpOCA +AQMWCQBo3869u/fv4MNrd3DlQoMC3QlkSJFdvPv38LVDWJLBAYHwE1LE38+/+/UhGTAggHv5odDf +gfv9/seDgPAVeAKCELqnIAwU3BefgyZEqOF3E7rAQH8YlrDhiNt1uEIG6IGoH4kjmpjCBRaqaCCL +G7p4AgUDIhgiCTTW2AKOEe44Qo8a2khCBgNoKKQIREZopAgZxAjhkhs0CeGTG7Sn5IpW9vekAyRS +2eWBRl6Q44ZijhlfAQlQmeKIaarpHZsMTHABCxDQGKec3JH3QpIs7snndn6yAKaeXA7aZwuABppo +fAws0GiEhaKQJ40F3DkjfwVC8CaCAlCgAgIkJjDfCgdiOMGn/Q2w3gkZtPgqC6ma0ECECaBwa4QE +aOpCrSYAqeMJpEKYqw7ABnsmfwQ8aCwPySqLYKUb/kwAYbPQyoiCtQcOUMKHBwrgK7LaogBuuaxC +OkS0KEwa37EiLBufALPuwO4Jh/InwAixkknEvSe4C9+p3PY3rr3lpnDufguIcCmzRQAc7IHYLhxf +w/8mnILA74lg8cARa4xCsZxusMCBomZccgsfv0deuh2HvLKh/sLs3hJSvieuCwUzvIHN4tGXc3ih +vtDzmj8fSNLR8BWQdH9LH+g00OFF3d/UBx4cUcvuOc21eFRiouV+Xvvr0dDvlX21R/2uzTR89TqU +L3+5UoBgAxtRHd5/CHpLkd13i4D2e3hHRLKMY+9Hr0Nvx/fq3Pw57cng7/m9wQVObnIyhAiQwHF8 +/tQS8nDgI2wOYeh3CAvhuIBHiDEgqvdtwudkaz3GBPKaTcKuGgqAJRMZmK6h1hnk3ncDcUvhgPFS +o5B476ZKQcECzCN4qgmYN4lAncmzcAEEkhJp+QlfkyhAAdtbN8H67FvHQAF6b4g6v9UryqfkKkBu +v/0prxD//kR63YnqB8AeqcdoBRxU/1zAuwRaaX4reJ4DSSRAHUhwgrgqwgUx2B94EWGDHISPBzUY +QgSNcAn6K6F4fscDCtBOhdoRwPW6kIHDwZA7vWoDBF44Qd/tIUAEBCACbIeG4AXxfmFrQ4B4OCYE +JBEQELChmgbAACJioj4JOCKCCLCABZ6EAg1IHwDlyLYAB1gRJhSYgHUQAD9WnQ9+CWBAA+wknTpC +JwQAOw== +} + +proc git_logo {w} { + label $w \ + -borderwidth 1 \ + -relief sunken \ + -background white \ + -image ::git_logo_data + return $w +} diff --git a/git-gui/lib/merge.tcl b/git-gui/lib/merge.tcl new file mode 100644 index 0000000..664803c --- /dev/null +++ b/git-gui/lib/merge.tcl @@ -0,0 +1,285 @@ +# git-gui branch merge support +# Copyright (C) 2006, 2007 Shawn Pearce + +class merge { + +field w ; # top level window +field w_rev ; # mega-widget to pick the revision to merge + +method _can_merge {} { + global HEAD commit_type file_states + + if {[string match amend* $commit_type]} { + info_popup [mc "Cannot merge while amending. + +You must finish amending this commit before starting any type of merge. +"] + return 0 + } + + if {[committer_ident] eq {}} {return 0} + if {![lock_index merge]} {return 0} + + # -- Our in memory state should match the repository. + # + repository_state curType curHEAD curMERGE_HEAD + if {$commit_type ne $curType || $HEAD ne $curHEAD} { + info_popup [mc "Last scanned state does not match repository state. + +Another Git program has modified this repository since the last scan. A rescan must be performed before a merge can be performed. + +The rescan will be automatically started now. +"] + unlock_index + rescan ui_ready + return 0 + } + + foreach path [array names file_states] { + switch -glob -- [lindex $file_states($path) 0] { + _O { + continue; # and pray it works! + } + _U - + U? { + error_popup [mc "You are in the middle of a conflicted merge. + +File %s has merge conflicts. + +You must resolve them, stage the file, and commit to complete the current merge. Only then can you begin another merge. +" [short_path $path]] + unlock_index + return 0 + } + ?? { + error_popup [mc "You are in the middle of a change. + +File %s is modified. + +You should complete the current commit before starting a merge. Doing so will help you abort a failed merge, should the need arise. +" [short_path $path]] + unlock_index + return 0 + } + } + } + + return 1 +} + +method _rev {} { + if {[catch {$w_rev commit_or_die}]} { + return {} + } + return [$w_rev get] +} + +method _visualize {} { + set rev [_rev $this] + if {$rev ne {}} { + do_gitk [list $rev --not HEAD] + } +} + +method _start {} { + global HEAD current_branch remote_url + global _last_merged_branch + + set name [_rev $this] + if {$name eq {}} { + return + } + + set spec [$w_rev get_tracking_branch] + set cmit [$w_rev get_commit] + + set fh [open [gitdir FETCH_HEAD] w] + fconfigure $fh -translation lf + if {$spec eq {}} { + set remote . + set branch $name + set stitle $branch + } else { + set remote $remote_url([lindex $spec 1]) + if {[regexp {^[^:@]*@[^:]*:/} $remote]} { + regsub {^[^:@]*@} $remote {} remote + } + set branch [lindex $spec 2] + set stitle [mc "%s of %s" $branch $remote] + } + regsub ^refs/heads/ $branch {} branch + puts $fh "$cmit\t\tbranch '$branch' of $remote" + close $fh + set _last_merged_branch $branch + + if {[git-version >= "2.5.0"]} { + set cmd [list git merge --strategy=recursive FETCH_HEAD] + } else { + set cmd [list git] + lappend cmd merge + lappend cmd --strategy=recursive + lappend cmd [git fmt-merge-msg <[gitdir FETCH_HEAD]] + lappend cmd HEAD + lappend cmd $name + } + + ui_status [mc "Merging %s and %s..." $current_branch $stitle] + set cons [console::new [mc "Merge"] "merge $stitle"] + console::exec $cons $cmd [cb _finish $cons] + + wm protocol $w WM_DELETE_WINDOW {} + destroy $w +} + +method _finish {cons ok} { + console::done $cons $ok + if {$ok} { + set msg [mc "Merge completed successfully."] + } else { + set msg [mc "Merge failed. Conflict resolution is required."] + } + unlock_index + rescan [list ui_status $msg] + delete_this +} + +constructor dialog {} { + global current_branch + global M1B use_ttk NS + + if {![_can_merge $this]} { + delete_this + return + } + + make_dialog top w + wm title $top [mc "%s (%s): Merge" [appname] [reponame]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + } + + set _start [cb _start] + + ${NS}::label $w.header \ + -text [mc "Merge Into %s" $current_branch] \ + -font font_uibold + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.visualize \ + -text [mc Visualize] \ + -command [cb _visualize] + pack $w.buttons.visualize -side left + ${NS}::button $w.buttons.merge \ + -text [mc Merge] \ + -command $_start + pack $w.buttons.merge -side right + ${NS}::button $w.buttons.cancel \ + -text [mc "Cancel"] \ + -command [cb _cancel] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + set w_rev [::choose_rev::new_unmerged $w.rev [mc "Revision To Merge"]] + pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5 + + bind $w <$M1B-Key-Return> $_start + bind $w <Key-Return> $_start + bind $w <Key-Escape> [cb _cancel] + wm protocol $w WM_DELETE_WINDOW [cb _cancel] + + bind $w.buttons.merge <Visibility> [cb _visible] + tkwait window $w +} + +method _visible {} { + grab $w + if {[is_config_true gui.matchtrackingbranch]} { + $w_rev pick_tracking_branch + } + $w_rev focus_filter +} + +method _cancel {} { + wm protocol $w WM_DELETE_WINDOW {} + unlock_index + destroy $w + delete_this +} + +} + +namespace eval merge { + +proc reset_hard {} { + global HEAD commit_type file_states + + if {[string match amend* $commit_type]} { + info_popup [mc "Cannot abort while amending. + +You must finish amending this commit. +"] + return + } + + if {![lock_index abort]} return + + if {[string match *merge* $commit_type]} { + set op_question [mc "Abort merge? + +Aborting the current merge will cause *ALL* uncommitted changes to be lost. + +Continue with aborting the current merge?"] + } else { + set op_question [mc "Reset changes? + +Resetting the changes will cause *ALL* uncommitted changes to be lost. + +Continue with resetting the current changes?"] + } + + if {[ask_popup $op_question] eq {yes}} { + set fd [git_read --stderr read-tree --reset -u -v HEAD] + fconfigure $fd -blocking 0 -translation binary + set status_bar_operation [$::main_status \ + start \ + [mc "Aborting"] \ + [mc "files reset"]] + fileevent $fd readable [namespace code [list \ + _reset_wait $fd $status_bar_operation]] + } else { + unlock_index + } +} + +proc _reset_wait {fd status_bar_operation} { + global ui_comm + + $status_bar_operation update_meter [read $fd] + + fconfigure $fd -blocking 1 + if {[eof $fd]} { + set fail [catch {close $fd} err] + unlock_index + $status_bar_operation stop + + $ui_comm delete 0.0 end + $ui_comm edit modified false + + catch {file delete [gitdir MERGE_HEAD]} + catch {file delete [gitdir rr-cache MERGE_RR]} + catch {file delete [gitdir MERGE_RR]} + catch {file delete [gitdir SQUASH_MSG]} + catch {file delete [gitdir MERGE_MSG]} + catch {file delete [gitdir GITGUI_MSG]} + + if {$fail} { + warn_popup "[mc "Abort failed."]\n\n$err" + } + rescan {ui_status [mc "Abort completed. Ready."]} + } else { + fconfigure $fd -blocking 0 + } +} + +} diff --git a/git-gui/lib/mergetool.tcl b/git-gui/lib/mergetool.tcl new file mode 100644 index 0000000..120bc40 --- /dev/null +++ b/git-gui/lib/mergetool.tcl @@ -0,0 +1,400 @@ +# git-gui merge conflict resolution +# parts based on git-mergetool (c) 2006 Theodore Y. Ts'o + +proc merge_resolve_one {stage} { + global current_diff_path + + switch -- $stage { + 1 { set targetquestion [mc "Force resolution to the base version?"] } + 2 { set targetquestion [mc "Force resolution to this branch?"] } + 3 { set targetquestion [mc "Force resolution to the other branch?"] } + } + + set op_question [strcat $targetquestion "\n" \ +[mc "Note that the diff shows only conflicting changes. + +%s will be overwritten. + +This operation can be undone only by restarting the merge." \ + [short_path $current_diff_path]]] + + if {[ask_popup $op_question] eq {yes}} { + merge_load_stages $current_diff_path [list merge_force_stage $stage] + } +} + +proc merge_stage_workdir {path {lno {}}} { + global current_diff_path diff_active + global current_diff_side ui_workdir + + if {$diff_active} return + + if {$path ne $current_diff_path || $ui_workdir ne $current_diff_side} { + show_diff $path $ui_workdir $lno {} [list do_merge_stage_workdir $path] + } else { + do_merge_stage_workdir $path + } +} + +proc do_merge_stage_workdir {path} { + global current_diff_path is_conflict_diff + + if {$path ne $current_diff_path} return; + + if {$is_conflict_diff} { + if {[ask_popup [mc "File %s seems to have unresolved conflicts, still stage?" \ + [short_path $path]]] ne {yes}} { + return + } + } + + merge_add_resolution $path +} + +proc merge_add_resolution {path} { + global current_diff_path ui_workdir + + set after [next_diff_after_action $ui_workdir $path {} {^_?U}] + + update_index \ + [mc "Adding resolution for %s" [short_path $path]] \ + [list $path] \ + [concat $after [list ui_ready]] +} + +proc merge_force_stage {stage} { + global current_diff_path merge_stages + + if {$merge_stages($stage) ne {}} { + git checkout-index -f --stage=$stage -- $current_diff_path + } else { + file delete -- $current_diff_path + } + + merge_add_resolution $current_diff_path +} + +proc merge_load_stages {path cont} { + global merge_stages_fd merge_stages merge_stages_buf + + if {[info exists merge_stages_fd]} { + catch { kill_file_process $merge_stages_fd } + catch { close $merge_stages_fd } + } + + set merge_stages(0) {} + set merge_stages(1) {} + set merge_stages(2) {} + set merge_stages(3) {} + set merge_stages_buf {} + + set merge_stages_fd [eval git_read ls-files -u -z -- {$path}] + + fconfigure $merge_stages_fd -blocking 0 -translation binary -encoding binary + fileevent $merge_stages_fd readable [list read_merge_stages $merge_stages_fd $cont] +} + +proc read_merge_stages {fd cont} { + global merge_stages_buf merge_stages_fd merge_stages + + append merge_stages_buf [read $fd] + set pck [split $merge_stages_buf "\0"] + set merge_stages_buf [lindex $pck end] + + if {[eof $fd] && $merge_stages_buf ne {}} { + lappend pck {} + set merge_stages_buf {} + } + + foreach p [lrange $pck 0 end-1] { + set fcols [split $p "\t"] + set cols [split [lindex $fcols 0] " "] + set stage [lindex $cols 2] + + set merge_stages($stage) [lrange $cols 0 1] + } + + if {[eof $fd]} { + close $fd + unset merge_stages_fd + eval $cont + } +} + +proc merge_resolve_tool {} { + global current_diff_path + + merge_load_stages $current_diff_path [list merge_resolve_tool2] +} + +proc merge_resolve_tool2 {} { + global current_diff_path merge_stages + + # Validate the stages + if {$merge_stages(2) eq {} || + [lindex $merge_stages(2) 0] eq {120000} || + [lindex $merge_stages(2) 0] eq {160000} || + $merge_stages(3) eq {} || + [lindex $merge_stages(3) 0] eq {120000} || + [lindex $merge_stages(3) 0] eq {160000} + } { + error_popup [mc "Cannot resolve deletion or link conflicts using a tool"] + return + } + + if {![file exists $current_diff_path]} { + error_popup [mc "Conflict file does not exist"] + return + } + + # Determine the tool to use + set tool [get_config merge.tool] + if {$tool eq {}} { set tool meld } + + set merge_tool_path [get_config "mergetool.$tool.path"] + if {$merge_tool_path eq {}} { + switch -- $tool { + emerge { set merge_tool_path "emacs" } + araxis { set merge_tool_path "compare" } + default { set merge_tool_path $tool } + } + } + + # Make file names + set filebase [file rootname $current_diff_path] + set fileext [file extension $current_diff_path] + set basename [lindex [file split $current_diff_path] end] + + set MERGED $current_diff_path + set BASE "./$MERGED.BASE$fileext" + set LOCAL "./$MERGED.LOCAL$fileext" + set REMOTE "./$MERGED.REMOTE$fileext" + set BACKUP "./$MERGED.BACKUP$fileext" + + set base_stage $merge_stages(1) + + # Build the command line + switch -- $tool { + araxis { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" -wait -merge -3 -a1 \ + -title1:"'$MERGED (Base)'" -title2:"'$MERGED (Local)'" \ + -title3:"'$MERGED (Remote)'" \ + "$BASE" "$LOCAL" "$REMOTE" "$MERGED"] + } else { + set cmdline [list "$merge_tool_path" -wait -2 \ + -title1:"'$MERGED (Local)'" -title2:"'$MERGED (Remote)'" \ + "$LOCAL" "$REMOTE" "$MERGED"] + } + } + bc3 { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" "$BASE" "-mergeoutput=$MERGED"] + } else { + set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" "-mergeoutput=$MERGED"] + } + } + ecmerge { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" "$BASE" "$LOCAL" "$REMOTE" --default --mode=merge3 --to="$MERGED"] + } else { + set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" --default --mode=merge2 --to="$MERGED"] + } + } + emerge { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" -f emerge-files-with-ancestor-command \ + "$LOCAL" "$REMOTE" "$BASE" "$basename"] + } else { + set cmdline [list "$merge_tool_path" -f emerge-files-command \ + "$LOCAL" "$REMOTE" "$basename"] + } + } + gvimdiff { + set cmdline [list "$merge_tool_path" -f "$LOCAL" "$MERGED" "$REMOTE"] + } + kdiff3 { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" --auto --L1 "$MERGED (Base)" \ + --L2 "$MERGED (Local)" --L3 "$MERGED (Remote)" -o "$MERGED" "$BASE" "$LOCAL" "$REMOTE"] + } else { + set cmdline [list "$merge_tool_path" --auto --L1 "$MERGED (Local)" \ + --L2 "$MERGED (Remote)" -o "$MERGED" "$LOCAL" "$REMOTE"] + } + } + meld { + set cmdline [list "$merge_tool_path" "$LOCAL" "$MERGED" "$REMOTE"] + } + opendiff { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" -ancestor "$BASE" -merge "$MERGED"] + } else { + set cmdline [list "$merge_tool_path" "$LOCAL" "$REMOTE" -merge "$MERGED"] + } + } + p4merge { + set cmdline [list "$merge_tool_path" "$BASE" "$REMOTE" "$LOCAL" "$MERGED"] + } + tkdiff { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" -a "$BASE" -o "$MERGED" "$LOCAL" "$REMOTE"] + } else { + set cmdline [list "$merge_tool_path" -o "$MERGED" "$LOCAL" "$REMOTE"] + } + } + vimdiff { + error_popup [mc "Not a GUI merge tool: '%s'" $tool] + return + } + winmerge { + if {$base_stage ne {}} { + # This tool does not support 3-way merges. + # Use the 'conflict file' resolution feature instead. + set cmdline [list "$merge_tool_path" -e -ub "$MERGED"] + } else { + set cmdline [list "$merge_tool_path" -e -ub -wl \ + -dl "Theirs File" -dr "Mine File" "$REMOTE" "$LOCAL" "$MERGED"] + } + } + xxdiff { + if {$base_stage ne {}} { + set cmdline [list "$merge_tool_path" -X --show-merged-pane \ + -R {Accel.SaveAsMerged: "Ctrl-S"} \ + -R {Accel.Search: "Ctrl+F"} \ + -R {Accel.SearchForward: "Ctrl-G"} \ + --merged-file "$MERGED" "$LOCAL" "$BASE" "$REMOTE"] + } else { + set cmdline [list "$merge_tool_path" -X --show-merged-pane \ + -R {Accel.SaveAsMerged: "Ctrl-S"} \ + -R {Accel.Search: "Ctrl+F"} \ + -R {Accel.SearchForward: "Ctrl-G"} \ + --merged-file "$MERGED" "$LOCAL" "$REMOTE"] + } + } + default { + error_popup [mc "Unsupported merge tool '%s'" $tool] + return + } + } + + merge_tool_start $cmdline $MERGED $BACKUP [list $BASE $LOCAL $REMOTE] +} + +proc delete_temp_files {files} { + foreach fname $files { + file delete $fname + } +} + +proc merge_tool_get_stages {target stages} { + global merge_stages + + set i 1 + foreach fname $stages { + if {$merge_stages($i) eq {}} { + file delete $fname + catch { close [open $fname w] } + } else { + # A hack to support autocrlf properly + git checkout-index -f --stage=$i -- $target + file rename -force -- $target $fname + } + incr i + } +} + +proc merge_tool_start {cmdline target backup stages} { + global merge_stages mtool_target mtool_tmpfiles mtool_fd mtool_mtime + + if {[info exists mtool_fd]} { + if {[ask_popup [mc "Merge tool is already running, terminate it?"]] eq {yes}} { + catch { kill_file_process $mtool_fd } + catch { close $mtool_fd } + unset mtool_fd + + set old_backup [lindex $mtool_tmpfiles end] + file rename -force -- $old_backup $mtool_target + delete_temp_files $mtool_tmpfiles + } else { + return + } + } + + # Save the original file + file rename -force -- $target $backup + + # Get the blobs; it destroys $target + if {[catch {merge_tool_get_stages $target $stages} err]} { + file rename -force -- $backup $target + delete_temp_files $stages + error_popup [mc "Error retrieving versions:\n%s" $err] + return + } + + # Restore the conflict file + file copy -force -- $backup $target + + # Initialize global state + set mtool_target $target + set mtool_mtime [file mtime $target] + set mtool_tmpfiles $stages + + lappend mtool_tmpfiles $backup + + # Force redirection to avoid interpreting output on stderr + # as an error, and launch the tool + lappend cmdline {2>@1} + + if {[catch { set mtool_fd [_open_stdout_stderr $cmdline] } err]} { + delete_temp_files $mtool_tmpfiles + error_popup [mc "Could not start the merge tool:\n\n%s" $err] + return + } + + ui_status [mc "Running merge tool..."] + + fconfigure $mtool_fd -blocking 0 -translation binary -encoding binary + fileevent $mtool_fd readable [list read_mtool_output $mtool_fd] +} + +proc read_mtool_output {fd} { + global mtool_fd mtool_tmpfiles + + read $fd + if {[eof $fd]} { + unset mtool_fd + + fconfigure $fd -blocking 1 + merge_tool_finish $fd + } +} + +proc merge_tool_finish {fd} { + global mtool_tmpfiles mtool_target mtool_mtime + + set backup [lindex $mtool_tmpfiles end] + set failed 0 + + # Check the return code + if {[catch {close $fd} err]} { + set failed 1 + if {$err ne {child process exited abnormally}} { + error_popup [strcat [mc "Merge tool failed."] "\n\n$err"] + } + } + + # Finish + if {$failed} { + file rename -force -- $backup $mtool_target + delete_temp_files $mtool_tmpfiles + ui_status [mc "Merge tool failed."] + } else { + if {[is_config_true mergetool.keepbackup]} { + file rename -force -- $backup "$mtool_target.orig" + } + + delete_temp_files $mtool_tmpfiles + + reshow_diff + } +} diff --git a/git-gui/lib/option.tcl b/git-gui/lib/option.tcl new file mode 100644 index 0000000..e43971b --- /dev/null +++ b/git-gui/lib/option.tcl @@ -0,0 +1,349 @@ +# git-gui options editor +# Copyright (C) 2006, 2007 Shawn Pearce + +proc config_check_encodings {} { + global repo_config_new global_config_new + + set enc $global_config_new(gui.encoding) + if {$enc eq {}} { + set global_config_new(gui.encoding) [encoding system] + } elseif {[tcl_encoding $enc] eq {}} { + error_popup [mc "Invalid global encoding '%s'" $enc] + return 0 + } + + set enc $repo_config_new(gui.encoding) + if {$enc eq {}} { + set repo_config_new(gui.encoding) [encoding system] + } elseif {[tcl_encoding $enc] eq {}} { + error_popup [mc "Invalid repo encoding '%s'" $enc] + return 0 + } + + return 1 +} + +proc save_config {} { + global default_config font_descs + global repo_config global_config system_config + global repo_config_new global_config_new + global ui_comm_spell + + foreach option $font_descs { + set name [lindex $option 0] + set font [lindex $option 1] + font configure $font \ + -family $global_config_new(gui.$font^^family) \ + -size $global_config_new(gui.$font^^size) + font configure ${font}bold \ + -family $global_config_new(gui.$font^^family) \ + -size $global_config_new(gui.$font^^size) + font configure ${font}italic \ + -family $global_config_new(gui.$font^^family) \ + -size $global_config_new(gui.$font^^size) + set global_config_new(gui.$name) [font configure $font] + unset global_config_new(gui.$font^^family) + unset global_config_new(gui.$font^^size) + } + + foreach name [array names default_config] { + set value $global_config_new($name) + if {$value ne $global_config($name)} { + if {$value eq $system_config($name)} { + catch {git config --global --unset $name} + } else { + regsub -all "\[{}\]" $value {"} value + git config --global $name $value + } + set global_config($name) $value + if {$value eq $repo_config($name)} { + catch {git config --unset $name} + set repo_config($name) $value + } + } + } + + foreach name [array names default_config] { + set value $repo_config_new($name) + if {$value ne $repo_config($name)} { + if {$value eq $global_config($name)} { + catch {git config --unset $name} + } else { + regsub -all "\[{}\]" $value {"} value + git config $name $value + } + set repo_config($name) $value + } + } + + if {[info exists repo_config(gui.spellingdictionary)]} { + set value $repo_config(gui.spellingdictionary) + if {$value eq {none}} { + if {[info exists ui_comm_spell]} { + $ui_comm_spell stop + } + } elseif {[info exists ui_comm_spell]} { + $ui_comm_spell lang $value + } + } +} + +proc do_options {} { + global repo_config global_config font_descs + global repo_config_new global_config_new + global ui_comm_spell use_ttk NS + + array unset repo_config_new + array unset global_config_new + foreach name [array names repo_config] { + set repo_config_new($name) $repo_config($name) + } + load_config 1 + foreach name [array names repo_config] { + switch -- $name { + gui.diffcontext {continue} + } + set repo_config_new($name) $repo_config($name) + } + foreach name [array names global_config] { + set global_config_new($name) $global_config($name) + } + + set w .options_editor + Dialog $w + wm withdraw $w + wm transient $w [winfo parent $w] + wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.restore -text [mc "Restore Defaults"] \ + -default normal \ + -command do_restore_defaults + pack $w.buttons.restore -side left + ${NS}::button $w.buttons.save -text [mc Save] \ + -default active \ + -command [list do_save_config $w] + pack $w.buttons.save -side right + ${NS}::button $w.buttons.cancel -text [mc "Cancel"] \ + -default normal \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::labelframe $w.repo -text [mc "%s Repository" [reponame]] + ${NS}::labelframe $w.global -text [mc "Global (All Repositories)"] + pack $w.repo -side left -fill both -expand 1 -pady 5 -padx 5 + pack $w.global -side right -fill both -expand 1 -pady 5 -padx 5 + + set optid 0 + foreach option { + {t user.name {mc "User Name"}} + {t user.email {mc "Email Address"}} + + {b merge.summary {mc "Summarize Merge Commits"}} + {i-1..5 merge.verbosity {mc "Merge Verbosity"}} + {b merge.diffstat {mc "Show Diffstat After Merge"}} + {t merge.tool {mc "Use Merge Tool"}} + + {b gui.trustmtime {mc "Trust File Modification Timestamps"}} + {b gui.pruneduringfetch {mc "Prune Tracking Branches During Fetch"}} + {b gui.matchtrackingbranch {mc "Match Tracking Branches"}} + {b gui.textconv {mc "Use Textconv For Diffs and Blames"}} + {b gui.fastcopyblame {mc "Blame Copy Only On Changed Files"}} + {i-0..100 gui.maxrecentrepo {mc "Maximum Length of Recent Repositories List"}} + {i-20..200 gui.copyblamethreshold {mc "Minimum Letters To Blame Copy On"}} + {i-0..300 gui.blamehistoryctx {mc "Blame History Context Radius (days)"}} + {i-1..99 gui.diffcontext {mc "Number of Diff Context Lines"}} + {t gui.diffopts {mc "Additional Diff Parameters"}} + {i-0..99 gui.commitmsgwidth {mc "Commit Message Text Width"}} + {t gui.newbranchtemplate {mc "New Branch Name Template"}} + {c gui.encoding {mc "Default File Contents Encoding"}} + {b gui.warndetachedcommit {mc "Warn before committing to a detached head"}} + {s gui.stageuntracked {mc "Staging of untracked files"} {list "yes" "no" "ask"}} + {b gui.displayuntracked {mc "Show untracked files"}} + {i-1..99 gui.tabsize {mc "Tab spacing"}} + } { + set type [lindex $option 0] + set name [lindex $option 1] + set text [eval [lindex $option 2]] + incr optid + foreach f {repo global} { + switch -glob -- $type { + b { + ${NS}::checkbutton $w.$f.$optid -text $text \ + -variable ${f}_config_new($name) \ + -onvalue true \ + -offvalue false + pack $w.$f.$optid -side top -anchor w + } + i-* { + regexp -- {-(\d+)\.\.(\d+)$} $type _junk min max + ${NS}::frame $w.$f.$optid + ${NS}::label $w.$f.$optid.l -text [mc "%s:" $text] + pack $w.$f.$optid.l -side left -anchor w -fill x + tspinbox $w.$f.$optid.v \ + -textvariable ${f}_config_new($name) \ + -from $min \ + -to $max \ + -increment 1 \ + -width [expr {1 + [string length $max]}] + bind $w.$f.$optid.v <FocusIn> {%W selection range 0 end} + pack $w.$f.$optid.v -side right -anchor e -padx 5 + pack $w.$f.$optid -side top -anchor w -fill x + } + c - + t { + ${NS}::frame $w.$f.$optid + ${NS}::label $w.$f.$optid.l -text [mc "%s:" $text] + ${NS}::entry $w.$f.$optid.v \ + -width 20 \ + -textvariable ${f}_config_new($name) + pack $w.$f.$optid.l -side left -anchor w + pack $w.$f.$optid.v -side left -anchor w \ + -fill x -expand 1 \ + -padx 5 + if {$type eq {c}} { + menu $w.$f.$optid.m + build_encoding_menu $w.$f.$optid.m \ + [list set ${f}_config_new($name)] 1 + ${NS}::button $w.$f.$optid.b \ + -text [mc "Change"] \ + -command [list popup_btn_menu \ + $w.$f.$optid.m $w.$f.$optid.b] + pack $w.$f.$optid.b -side left -anchor w + } + pack $w.$f.$optid -side top -anchor w -fill x + } + s { + set opts [eval [lindex $option 3]] + ${NS}::frame $w.$f.$optid + ${NS}::label $w.$f.$optid.l -text [mc "%s:" $text] + if {$use_ttk} { + ttk::combobox $w.$f.$optid.v \ + -textvariable ${f}_config_new($name) \ + -values $opts -state readonly + } else { + eval tk_optionMenu $w.$f.$optid.v \ + ${f}_config_new($name) \ + $opts + } + pack $w.$f.$optid.l -side left -anchor w -fill x + pack $w.$f.$optid.v -side right -anchor e -padx 5 + pack $w.$f.$optid -side top -anchor w -fill x + } + } + } + } + + set all_dicts [linsert \ + [spellcheck::available_langs] \ + 0 \ + none] + incr optid + foreach f {repo global} { + if {![info exists ${f}_config_new(gui.spellingdictionary)]} { + if {[info exists ui_comm_spell]} { + set value [$ui_comm_spell lang] + } else { + set value none + } + set ${f}_config_new(gui.spellingdictionary) $value + } + + ${NS}::frame $w.$f.$optid + ${NS}::label $w.$f.$optid.l -text [mc "Spelling Dictionary:"] + if {$use_ttk} { + ttk::combobox $w.$f.$optid.v \ + -textvariable ${f}_config_new(gui.spellingdictionary) \ + -values $all_dicts -state readonly + } else { + eval tk_optionMenu $w.$f.$optid.v \ + ${f}_config_new(gui.spellingdictionary) \ + $all_dicts + } + pack $w.$f.$optid.l -side left -anchor w -fill x + pack $w.$f.$optid.v -side right -anchor e -padx 5 + pack $w.$f.$optid -side top -anchor w -fill x + } + unset all_dicts + + set all_fonts [lsort [font families]] + foreach option $font_descs { + set name [lindex $option 0] + set font [lindex $option 1] + set text [eval [lindex $option 2]] + + set global_config_new(gui.$font^^family) \ + [font configure $font -family] + set global_config_new(gui.$font^^size) \ + [font configure $font -size] + + ${NS}::frame $w.global.$name + ${NS}::label $w.global.$name.l -text [mc "%s:" $text] + ${NS}::button $w.global.$name.b \ + -text [mc "Change Font"] \ + -command [list \ + tchoosefont \ + $w \ + [mc "Choose %s" $text] \ + global_config_new(gui.$font^^family) \ + global_config_new(gui.$font^^size) \ + ] + ${NS}::label $w.global.$name.f -textvariable global_config_new(gui.$font^^family) + ${NS}::label $w.global.$name.s -textvariable global_config_new(gui.$font^^size) + ${NS}::label $w.global.$name.pt -text [mc "pt."] + pack $w.global.$name.l -side left -anchor w + pack $w.global.$name.b -side right -anchor e + pack $w.global.$name.pt -side right -anchor w + pack $w.global.$name.s -side right -anchor w + pack $w.global.$name.f -side right -anchor w + pack $w.global.$name -side top -anchor w -fill x + } + + bind $w <Visibility> "grab $w; focus $w.buttons.save" + bind $w <Key-Escape> "destroy $w" + bind $w <Key-Return> [list do_save_config $w] + + if {[is_MacOSX]} { + set t [mc "Preferences"] + } else { + set t [mc "Options"] + } + wm title $w "[appname] ([reponame]): $t" + wm deiconify $w + tkwait window $w +} + +proc do_restore_defaults {} { + global font_descs default_config repo_config system_config + global repo_config_new global_config_new + + foreach name [array names default_config] { + set repo_config_new($name) $system_config($name) + set global_config_new($name) $system_config($name) + } + + foreach option $font_descs { + set name [lindex $option 0] + set repo_config(gui.$name) $system_config(gui.$name) + } + apply_config + + foreach option $font_descs { + set name [lindex $option 0] + set font [lindex $option 1] + set global_config_new(gui.$font^^family) \ + [font configure $font -family] + set global_config_new(gui.$font^^size) \ + [font configure $font -size] + } +} + +proc do_save_config {w} { + if {![config_check_encodings]} return + if {[catch {save_config} err]} { + error_popup [strcat [mc "Failed to completely save options:"] "\n\n$err"] + } + reshow_diff + destroy $w +} diff --git a/git-gui/lib/remote.tcl b/git-gui/lib/remote.tcl new file mode 100644 index 0000000..ef77ed7 --- /dev/null +++ b/git-gui/lib/remote.tcl @@ -0,0 +1,333 @@ +# git-gui remote management +# Copyright (C) 2006, 2007 Shawn Pearce + +set some_heads_tracking 0; # assume not + +proc is_tracking_branch {name} { + global tracking_branches + foreach spec $tracking_branches { + set t [lindex $spec 0] + if {$t eq $name || [string match $t $name]} { + return 1 + } + } + return 0 +} + +proc all_tracking_branches {} { + global tracking_branches + + set all [list] + set pat [list] + set cmd [list] + + foreach spec $tracking_branches { + set dst [lindex $spec 0] + if {[string range $dst end-1 end] eq {/*}} { + lappend pat $spec + lappend cmd [string range $dst 0 end-2] + } else { + lappend all $spec + } + } + + if {$pat ne {}} { + set fd [eval git_read for-each-ref --format=%(refname) $cmd] + while {[gets $fd n] > 0} { + foreach spec $pat { + set dst [string range [lindex $spec 0] 0 end-2] + set len [string length $dst] + if {[string equal -length $len $dst $n]} { + set src [string range [lindex $spec 2] 0 end-2] + set spec [list \ + $n \ + [lindex $spec 1] \ + $src[string range $n $len end] \ + ] + lappend all $spec + } + } + } + close $fd + } + + return [lsort -index 0 -unique $all] +} + +proc load_all_remotes {} { + global repo_config + global all_remotes tracking_branches some_heads_tracking + global remote_url + + set some_heads_tracking 0 + set all_remotes [list] + set trck [list] + + set rh_str refs/heads/ + set rh_len [string length $rh_str] + set rm_dir [gitdir remotes] + if {[file isdirectory $rm_dir]} { + set all_remotes [glob \ + -types f \ + -tails \ + -nocomplain \ + -directory $rm_dir *] + + foreach name $all_remotes { + catch { + set fd [open [file join $rm_dir $name] r] + while {[gets $fd line] >= 0} { + if {[regexp {^URL:[ ]*(.+)$} $line line url]} { + set remote_url($name) $url + continue + } + if {![regexp {^Pull:[ ]*([^:]+):(.+)$} \ + $line line src dst]} continue + if {[string index $src 0] eq {+}} { + set src [string range $src 1 end] + } + if {![string equal -length 5 refs/ $src]} { + set src $rh_str$src + } + if {![string equal -length 5 refs/ $dst]} { + set dst $rh_str$dst + } + if {[string equal -length $rh_len $rh_str $dst]} { + set some_heads_tracking 1 + } + lappend trck [list $dst $name $src] + } + close $fd + } + } + } + + foreach line [array names repo_config remote.*.url] { + if {![regexp ^remote\.(.*)\.url\$ $line line name]} continue + lappend all_remotes $name + set remote_url($name) $repo_config(remote.$name.url) + + if {[catch {set fl $repo_config(remote.$name.fetch)}]} { + set fl {} + } + foreach line $fl { + if {![regexp {^([^:]+):(.+)$} $line line src dst]} continue + if {[string index $src 0] eq {+}} { + set src [string range $src 1 end] + } + if {![string equal -length 5 refs/ $src]} { + set src $rh_str$src + } + if {![string equal -length 5 refs/ $dst]} { + set dst $rh_str$dst + } + if {[string equal -length $rh_len $rh_str $dst]} { + set some_heads_tracking 1 + } + lappend trck [list $dst $name $src] + } + } + + set tracking_branches [lsort -index 0 -unique $trck] + set all_remotes [lsort -unique $all_remotes] +} + +proc add_fetch_entry {r} { + global repo_config + set remote_m .mbar.remote + set fetch_m $remote_m.fetch + set prune_m $remote_m.prune + set remove_m $remote_m.remove + set enable 0 + if {![catch {set a $repo_config(remote.$r.url)}]} { + if {![catch {set a $repo_config(remote.$r.fetch)}]} { + set enable 1 + } + } else { + catch { + set fd [open [gitdir remotes $r] r] + while {[gets $fd n] >= 0} { + if {[regexp {^Pull:[ \t]*([^:]+):} $n]} { + set enable 1 + break + } + } + close $fd + } + } + + if {$enable} { + make_sure_remote_submenues_exist $remote_m + + $fetch_m add command \ + -label $r \ + -command [list fetch_from $r] + $prune_m add command \ + -label $r \ + -command [list prune_from $r] + $remove_m add command \ + -label $r \ + -command [list remove_remote $r] + } +} + +proc add_push_entry {r} { + global repo_config + set remote_m .mbar.remote + set push_m $remote_m.push + set enable 0 + if {![catch {set a $repo_config(remote.$r.url)}]} { + if {![catch {set a $repo_config(remote.$r.push)}]} { + set enable 1 + } + } else { + catch { + set fd [open [gitdir remotes $r] r] + while {[gets $fd n] >= 0} { + if {[regexp {^Push:[ \t]*([^:]+):} $n]} { + set enable 1 + break + } + } + close $fd + } + } + + if {$enable} { + if {![winfo exists $push_m]} { + menu $push_m + $remote_m insert 0 cascade \ + -label [mc "Push to"] \ + -menu $push_m + } + + $push_m add command \ + -label $r \ + -command [list push_to $r] + } +} + +proc make_sure_remote_submenues_exist {remote_m} { + set fetch_m $remote_m.fetch + set prune_m $remote_m.prune + set remove_m $remote_m.remove + + if {![winfo exists $fetch_m]} { + menu $remove_m + $remote_m insert 0 cascade \ + -label [mc "Remove Remote"] \ + -menu $remove_m + + menu $prune_m + $remote_m insert 0 cascade \ + -label [mc "Prune from"] \ + -menu $prune_m + + menu $fetch_m + $remote_m insert 0 cascade \ + -label [mc "Fetch from"] \ + -menu $fetch_m + } +} + +proc update_all_remotes_menu_entry {} { + global all_remotes + + if {[git-version < 1.6.6]} { return } + + set have_remote 0 + foreach r $all_remotes { + incr have_remote + } + + set remote_m .mbar.remote + set fetch_m $remote_m.fetch + set prune_m $remote_m.prune + if {$have_remote > 1} { + make_sure_remote_submenues_exist $remote_m + if {[$fetch_m type end] eq "command" \ + && [$fetch_m entrycget end -label] ne [mc "All"]} { + + $fetch_m insert end separator + $fetch_m insert end command \ + -label [mc "All"] \ + -command fetch_from_all + + $prune_m insert end separator + $prune_m insert end command \ + -label [mc "All"] \ + -command prune_from_all + } + } else { + if {[winfo exists $fetch_m]} { + if {[$fetch_m type end] eq "command" \ + && [$fetch_m entrycget end -label] eq [mc "All"]} { + + delete_from_menu $fetch_m end + delete_from_menu $fetch_m end + + delete_from_menu $prune_m end + delete_from_menu $prune_m end + } + } + } +} + +proc populate_remotes_menu {} { + global all_remotes + + foreach r $all_remotes { + add_fetch_entry $r + add_push_entry $r + } + + update_all_remotes_menu_entry +} + +proc add_single_remote {name location} { + global all_remotes repo_config + lappend all_remotes $name + + git remote add $name $location + + # XXX: Better re-read the config so that we will never get out + # of sync with git remote implementation? + set repo_config(remote.$name.url) $location + set repo_config(remote.$name.fetch) "+refs/heads/*:refs/remotes/$name/*" + + add_fetch_entry $name + add_push_entry $name + + update_all_remotes_menu_entry +} + +proc delete_from_menu {menu name} { + if {[winfo exists $menu]} { + $menu delete $name + } +} + +proc remove_remote {name} { + global all_remotes repo_config + + git remote rm $name + + catch { + # Missing values are ok + unset repo_config(remote.$name.url) + unset repo_config(remote.$name.fetch) + unset repo_config(remote.$name.push) + } + + set i [lsearch -exact $all_remotes $name] + set all_remotes [lreplace $all_remotes $i $i] + + set remote_m .mbar.remote + delete_from_menu $remote_m.fetch $name + delete_from_menu $remote_m.prune $name + delete_from_menu $remote_m.remove $name + # Not all remotes are in the push menu + catch { delete_from_menu $remote_m.push $name } + + update_all_remotes_menu_entry +} diff --git a/git-gui/lib/remote_add.tcl b/git-gui/lib/remote_add.tcl new file mode 100644 index 0000000..480a6b3 --- /dev/null +++ b/git-gui/lib/remote_add.tcl @@ -0,0 +1,190 @@ +# git-gui remote adding support +# Copyright (C) 2008 Petr Baudis + +class remote_add { + +field w ; # widget path +field w_name ; # new remote name widget +field w_loc ; # new remote location widget + +field name {}; # name of the remote the user has chosen +field location {}; # location of the remote the user has chosen + +field opt_action fetch; # action to do after registering the remote locally + +constructor dialog {} { + global repo_config use_ttk NS + + make_dialog top w + wm withdraw $top + wm title $top [mc "%s (%s): Add Remote" [appname] [reponame]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + } + + ${NS}::label $w.header -text [mc "Add New Remote"] \ + -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.create -text [mc Add] \ + -default active \ + -command [cb _add] + pack $w.buttons.create -side right + ${NS}::button $w.buttons.cancel -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::labelframe $w.desc -text [mc "Remote Details"] + + ${NS}::label $w.desc.name_l -text [mc "Name:"] + set w_name $w.desc.name_t + ${NS}::entry $w_name \ + -width 40 \ + -textvariable @name \ + -validate key \ + -validatecommand [cb _validate_name %d %S] + grid $w.desc.name_l $w_name -sticky we -padx {0 5} + + ${NS}::label $w.desc.loc_l -text [mc "Location:"] + set w_loc $w.desc.loc_t + ${NS}::entry $w_loc \ + -width 40 \ + -textvariable @location + grid $w.desc.loc_l $w_loc -sticky we -padx {0 5} + + grid columnconfigure $w.desc 1 -weight 1 + pack $w.desc -anchor nw -fill x -pady 5 -padx 5 + + ${NS}::labelframe $w.action -text [mc "Further Action"] + + ${NS}::radiobutton $w.action.fetch \ + -text [mc "Fetch Immediately"] \ + -value fetch \ + -variable @opt_action + pack $w.action.fetch -anchor nw + + ${NS}::radiobutton $w.action.push \ + -text [mc "Initialize Remote Repository and Push"] \ + -value push \ + -variable @opt_action + pack $w.action.push -anchor nw + + ${NS}::radiobutton $w.action.none \ + -text [mc "Do Nothing Else Now"] \ + -value none \ + -variable @opt_action + pack $w.action.none -anchor nw + + grid columnconfigure $w.action 1 -weight 1 + pack $w.action -anchor nw -fill x -pady 5 -padx 5 + + bind $w <Visibility> [cb _visible] + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _add]\;break + wm deiconify $top + tkwait window $w +} + +method _add {} { + global repo_config env + global M1B + + if {$name eq {}} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Please supply a remote name."] + focus $w_name + return + } + + # XXX: We abuse check-ref-format here, but + # that should be ok. + if {[catch {git check-ref-format "remotes/$name"}]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "'%s' is not an acceptable remote name." $name] + focus $w_name + return + } + + if {[catch {add_single_remote $name $location}]} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Failed to add remote '%s' of location '%s'." $name $location] + focus $w_name + return + } + + switch -- $opt_action { + fetch { + set c [console::new \ + [mc "fetch %s" $name] \ + [mc "Fetching the %s" $name]] + console::exec $c [list git fetch $name] + } + push { + set cmds [list] + + # Parse the location + if { [regexp {(?:git\+)?ssh://([^/]+)(/.+)} $location xx host path] + || [regexp {([^:][^:]+):(.+)} $location xx host path]} { + set ssh ssh + if {[info exists env(GIT_SSH)]} { + set ssh $env(GIT_SSH) + } + lappend cmds [list exec $ssh $host mkdir -p $location && git --git-dir=$path init --bare] + } elseif { ! [regexp {://} $location xx] } { + lappend cmds [list exec mkdir -p $location] + lappend cmds [list exec git --git-dir=$location init --bare] + } else { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Do not know how to initialize repository at location '%s'." $location] + destroy $w + return + } + + set c [console::new \ + [mc "push %s" $name] \ + [mc "Setting up the %s (at %s)" $name $location]] + + lappend cmds [list exec git push -v --all $name] + console::chain $c $cmds + } + none { + } + } + + destroy $w +} + +method _validate_name {d S} { + if {$d == 1} { + if {[regexp {[~^:?*\[\0- ]} $S]} { + return 0 + } + } + return 1 +} + +method _visible {} { + grab $w + $w_name icursor end + focus $w_name +} + +} diff --git a/git-gui/lib/remote_branch_delete.tcl b/git-gui/lib/remote_branch_delete.tcl new file mode 100644 index 0000000..5ba9fca --- /dev/null +++ b/git-gui/lib/remote_branch_delete.tcl @@ -0,0 +1,359 @@ +# git-gui remote branch deleting support +# Copyright (C) 2007 Shawn Pearce + +class remote_branch_delete { + +field w +field head_m + +field urltype {url} +field remote {} +field url {} + +field checktype {head} +field check_head {} + +field status {} +field idle_id {} +field full_list {} +field head_list {} +field active_ls {} +field head_cache +field full_cache +field cached + +constructor dialog {} { + global all_remotes M1B use_ttk NS + + make_dialog top w + wm title $top [mc "%s (%s): Delete Branch Remotely" [appname] [reponame]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + } + + ${NS}::label $w.header -text [mc "Delete Branch Remotely"] \ + -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.delete -text [mc Delete] \ + -default active \ + -command [cb _delete] + pack $w.buttons.delete -side right + ${NS}::button $w.buttons.cancel -text [mc "Cancel"] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::labelframe $w.dest -text [mc "From Repository"] + if {$all_remotes ne {}} { + ${NS}::radiobutton $w.dest.remote_r \ + -text [mc "Remote:"] \ + -value remote \ + -variable @urltype + if {$use_ttk} { + ttk::combobox $w.dest.remote_m -textvariable @remote \ + -values $all_remotes -state readonly + } else { + eval tk_optionMenu $w.dest.remote_m @remote $all_remotes + } + grid $w.dest.remote_r $w.dest.remote_m -sticky w + if {[lsearch -sorted -exact $all_remotes origin] != -1} { + set remote origin + } else { + set remote [lindex $all_remotes 0] + } + set urltype remote + trace add variable @remote write [cb _write_remote] + } else { + set urltype url + } + ${NS}::radiobutton $w.dest.url_r \ + -text [mc "Arbitrary Location:"] \ + -value url \ + -variable @urltype + ${NS}::entry $w.dest.url_t \ + -width 50 \ + -textvariable @url \ + -validate key \ + -validatecommand { + if {%d == 1 && [regexp {\s} %S]} {return 0} + return 1 + } + trace add variable @url write [cb _write_url] + grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5} + grid columnconfigure $w.dest 1 -weight 1 + pack $w.dest -anchor nw -fill x -pady 5 -padx 5 + + ${NS}::labelframe $w.heads -text [mc "Branches"] + slistbox $w.heads.l \ + -height 10 \ + -width 70 \ + -listvariable @head_list \ + -selectmode extended + + ${NS}::frame $w.heads.footer + ${NS}::label $w.heads.footer.status \ + -textvariable @status \ + -anchor w \ + -justify left + ${NS}::button $w.heads.footer.rescan \ + -text [mc "Rescan"] \ + -command [cb _rescan] + pack $w.heads.footer.status -side left -fill x + pack $w.heads.footer.rescan -side right + + pack $w.heads.footer -side bottom -fill x + pack $w.heads.l -side left -fill both -expand 1 + pack $w.heads -fill both -expand 1 -pady 5 -padx 5 + + ${NS}::labelframe $w.validate -text [mc "Delete Only If"] + ${NS}::radiobutton $w.validate.head_r \ + -text [mc "Merged Into:"] \ + -value head \ + -variable @checktype + set head_m [tk_optionMenu $w.validate.head_m @check_head {}] + trace add variable @head_list write [cb _write_head_list] + trace add variable @check_head write [cb _write_check_head] + grid $w.validate.head_r $w.validate.head_m -sticky w + ${NS}::radiobutton $w.validate.always_r \ + -text [mc "Always (Do not perform merge checks)"] \ + -value always \ + -variable @checktype + grid $w.validate.always_r -columnspan 2 -sticky w + grid columnconfigure $w.validate 1 -weight 1 + pack $w.validate -anchor nw -fill x -pady 5 -padx 5 + + trace add variable @urltype write [cb _write_urltype] + _rescan $this + + bind $w <Key-F5> [cb _rescan] + bind $w <$M1B-Key-r> [cb _rescan] + bind $w <$M1B-Key-R> [cb _rescan] + bind $w <Key-Return> [cb _delete] + bind $w <Key-Escape> [list destroy $w] + return $w +} + +method _delete {} { + switch $urltype { + remote {set uri $remote} + url {set uri $url} + } + + set cache $urltype:$uri + set crev {} + if {$checktype eq {head}} { + if {$check_head eq {}} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "A branch is required for 'Merged Into'."] + return + } + set crev $full_cache("$cache\nrefs/heads/$check_head") + } + + set not_merged [list] + set need_fetch 0 + set have_selection 0 + set push_cmd [list git push] + lappend push_cmd -v + lappend push_cmd $uri + + foreach i [$w.heads.l curselection] { + set ref [lindex $full_list $i] + if {$crev ne {}} { + set obj $full_cache("$cache\n$ref") + if {[catch {set m [git merge-base $obj $crev]}]} { + set need_fetch 1 + set m {} + } + if {$obj ne $m} { + lappend not_merged [lindex $head_list $i] + continue + } + } + + lappend push_cmd :$ref + set have_selection 1 + } + + if {$not_merged ne {}} { + set msg [mc "The following branches are not completely merged into %s: + + - %s" $check_head [join $not_merged "\n - "]] + + if {$need_fetch} { + append msg "\n\n" [mc "One or more of the merge tests failed because you have not fetched the necessary commits. Try fetching from %s first." $uri] + } + + tk_messageBox \ + -icon info \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message $msg + if {!$have_selection} return + } + + if {!$have_selection} { + tk_messageBox \ + -icon error \ + -type ok \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Please select one or more branches to delete."] + return + } + + if {$checktype ne {head}} { + if {[tk_messageBox \ + -icon warning \ + -type yesno \ + -title [wm title $w] \ + -parent $w \ + -message [mc "Recovering deleted branches is difficult.\n\nDelete the selected branches?"]] ne yes} { + return + } + } + + destroy $w + + set cons [console::new \ + "push $uri" \ + [mc "Deleting branches from %s" $uri]] + console::exec $cons $push_cmd +} + +method _rescan {{force 1}} { + switch $urltype { + remote {set uri $remote} + url {set uri $url} + } + + if {$force} { + unset -nocomplain cached($urltype:$uri) + } + + if {$idle_id ne {}} { + after cancel $idle_id + set idle_id {} + } + + _load $this $urltype:$uri $uri +} + +method _write_remote {args} { set urltype remote } +method _write_url {args} { set urltype url } +method _write_check_head {args} { set checktype head } + +method _write_head_list {args} { + global current_branch _last_merged_branch + + $head_m delete 0 end + foreach abr $head_list { + $head_m insert end radiobutton \ + -label $abr \ + -value $abr \ + -variable @check_head + } + if {[lsearch -exact -sorted $head_list $check_head] < 0} { + if {[lsearch -exact -sorted $head_list $current_branch] < 0} { + set check_head {} + } else { + set check_head $current_branch + } + } + set lmb [lsearch -exact -sorted $head_list $_last_merged_branch] + if {$lmb >= 0} { + $w.heads.l conf -state normal + $w.heads.l select set $lmb + $w.heads.l yview $lmb + $w.heads.l conf -state disabled + } +} + +method _write_urltype {args} { + if {$urltype eq {url}} { + if {$idle_id ne {}} { + after cancel $idle_id + } + _load $this none: {} + set idle_id [after 1000 [cb _rescan 0]] + } else { + _rescan $this 0 + } +} + +method _load {cache uri} { + if {$active_ls ne {}} { + catch {close $active_ls} + } + + if {$uri eq {}} { + $w.heads.l conf -state disabled + set head_list [list] + set full_list [list] + set status [mc "No repository selected."] + return + } + + if {[catch {set x $cached($cache)}]} { + set status [mc "Scanning %s..." $uri] + $w.heads.l conf -state disabled + set head_list [list] + set full_list [list] + set head_cache($cache) [list] + set full_cache($cache) [list] + set active_ls [git_read ls-remote $uri] + fconfigure $active_ls \ + -blocking 0 \ + -translation lf \ + -encoding utf-8 + fileevent $active_ls readable [cb _read $cache $active_ls] + } else { + set status {} + set full_list $full_cache($cache) + set head_list $head_cache($cache) + $w.heads.l conf -state normal + } +} + +method _read {cache fd} { + if {$fd ne $active_ls} { + catch {close $fd} + return + } + + while {[gets $fd line] >= 0} { + if {[string match {*^{}} $line]} continue + if {[regexp {^([0-9a-f]{40}) (.*)$} $line _junk obj ref]} { + if {[regsub ^refs/heads/ $ref {} abr]} { + lappend head_list $abr + lappend head_cache($cache) $abr + lappend full_list $ref + lappend full_cache($cache) $ref + set full_cache("$cache\n$ref") $obj + } + } + } + + if {[eof $fd]} { + if {[catch {close $fd} err]} { + set status $err + set head_list [list] + set full_list [list] + } else { + set status {} + set cached($cache) 1 + $w.heads.l conf -state normal + } + } +} ifdeleted { + catch {close $fd} +} + +} diff --git a/git-gui/lib/search.tcl b/git-gui/lib/search.tcl new file mode 100644 index 0000000..ef1e555 --- /dev/null +++ b/git-gui/lib/search.tcl @@ -0,0 +1,300 @@ +# incremental search panel +# based on code from gitk, Copyright (C) Paul Mackerras + +class searchbar { + +field w +field ctext + +field searchstring {} +field regexpsearch +field default_regexpsearch +field casesensitive +field default_casesensitive +field smartcase +field searchdirn -forwards + +field history +field history_index + +field smarktop +field smarkbot + +constructor new {i_w i_text args} { + global use_ttk NS + set w $i_w + set ctext $i_text + + set default_regexpsearch [is_config_true gui.search.regexp] + switch -- [get_config gui.search.case] { + no { + set default_casesensitive 0 + set smartcase 0 + } + smart { + set default_casesensitive 0 + set smartcase 1 + } + yes - + default { + set default_casesensitive 1 + set smartcase 0 + } + } + + set history [list] + + ${NS}::frame $w + ${NS}::label $w.l -text [mc Find:] + tentry $w.ent -textvariable ${__this}::searchstring -background lightgreen + ${NS}::button $w.bn -text [mc Next] -command [cb find_next] + ${NS}::button $w.bp -text [mc Prev] -command [cb find_prev] + ${NS}::checkbutton $w.re -text [mc RegExp] \ + -variable ${__this}::regexpsearch -command [cb _incrsearch] + ${NS}::checkbutton $w.cs -text [mc Case] \ + -variable ${__this}::casesensitive -command [cb _incrsearch] + pack $w.l -side left + pack $w.cs -side right + pack $w.re -side right + pack $w.bp -side right + pack $w.bn -side right + pack $w.ent -side left -expand 1 -fill x + + eval grid conf $w -sticky we $args + grid remove $w + + trace add variable searchstring write [cb _incrsearch_cb] + bind $w.ent <Return> [cb find_next] + bind $w.ent <Shift-Return> [cb find_prev] + bind $w.ent <Key-Up> [cb _prev_search] + bind $w.ent <Key-Down> [cb _next_search] + + bind $w <Destroy> [list delete_this $this] + return $this +} + +method show {} { + if {![visible $this]} { + grid $w + $w.ent delete 0 end + set regexpsearch $default_regexpsearch + set casesensitive $default_casesensitive + set history_index [llength $history] + } + focus -force $w.ent +} + +method hide {} { + if {[visible $this]} { + focus $ctext + grid remove $w + _save_search $this + } +} + +method visible {} { + return [winfo ismapped $w] +} + +method editor {} { + return $w.ent +} + +method _get_new_anchor {} { + # use start of selection if it is visible, + # or the bounds of the visible area + set top [$ctext index @0,0] + set bottom [$ctext index @0,[winfo height $ctext]] + set sel [$ctext tag ranges sel] + if {$sel ne {}} { + set spos [lindex $sel 0] + if {[lindex $spos 0] >= [lindex $top 0] && + [lindex $spos 0] <= [lindex $bottom 0]} { + return $spos + } + } + if {$searchdirn eq "-forwards"} { + return $top + } else { + return $bottom + } +} + +method _get_wrap_anchor {dir} { + if {$dir eq "-forwards"} { + return 1.0 + } else { + return end + } +} + +method _do_search {start {mlenvar {}} {dir {}} {endbound {}}} { + set cmd [list $ctext search] + if {$mlenvar ne {}} { + upvar $mlenvar mlen + lappend cmd -count mlen + } + if {$regexpsearch} { + lappend cmd -regexp + } + if {!$casesensitive} { + lappend cmd -nocase + } + if {$dir eq {}} { + set dir $searchdirn + } + lappend cmd $dir -- $searchstring + if {[catch { + if {$endbound ne {}} { + set here [eval $cmd [list $start] [list $endbound]] + } else { + set here [eval $cmd [list $start]] + if {$here eq {}} { + set here [eval $cmd [_get_wrap_anchor $this $dir]] + } + } + } err]} { set here {} } + return $here +} + +method _incrsearch_cb {name ix op} { + after idle [cb _incrsearch] +} + +method _incrsearch {} { + $ctext tag remove found 1.0 end + if {[catch {$ctext index anchor}]} { + $ctext mark set anchor [_get_new_anchor $this] + } + if {$searchstring ne {}} { + if {$smartcase && [regexp {[[:upper:]]} $searchstring]} { + set casesensitive 1 + } + set here [_do_search $this anchor mlen] + if {$here ne {}} { + $ctext see $here + $ctext tag remove sel 1.0 end + $ctext tag add sel $here "$here + $mlen c" + #$w.ent configure -background lightgreen + $w.ent state !pressed + _set_marks $this 1 + } else { + #$w.ent configure -background lightpink + $w.ent state pressed + } + } elseif {$smartcase} { + # clearing the field resets the smart case detection + set casesensitive 0 + } +} + +method _save_search {} { + if {$searchstring eq {}} { + return + } + if {[llength $history] > 0} { + foreach {s_regexp s_case s_expr} [lindex $history end] break + } else { + set s_regexp $regexpsearch + set s_case $casesensitive + set s_expr "" + } + if {$searchstring eq $s_expr} { + # update modes + set history [lreplace $history end end \ + [list $regexpsearch $casesensitive $searchstring]] + } else { + lappend history [list $regexpsearch $casesensitive $searchstring] + } + set history_index [llength $history] +} + +method _prev_search {} { + if {$history_index > 0} { + incr history_index -1 + foreach {s_regexp s_case s_expr} [lindex $history $history_index] break + $w.ent delete 0 end + $w.ent insert 0 $s_expr + set regexpsearch $s_regexp + set casesensitive $s_case + } +} + +method _next_search {} { + if {$history_index < [llength $history]} { + incr history_index + } + if {$history_index < [llength $history]} { + foreach {s_regexp s_case s_expr} [lindex $history $history_index] break + } else { + set s_regexp $default_regexpsearch + set s_case $default_casesensitive + set s_expr "" + } + $w.ent delete 0 end + $w.ent insert 0 $s_expr + set regexpsearch $s_regexp + set casesensitive $s_case +} + +method find_prev {} { + find_next $this -backwards +} + +method find_next {{dir -forwards}} { + focus $w.ent + $w.ent icursor end + set searchdirn $dir + $ctext mark unset anchor + if {$searchstring ne {}} { + _save_search $this + set start [_get_new_anchor $this] + if {$dir eq "-forwards"} { + set start "$start + 1c" + } + set match [_do_search $this $start mlen] + $ctext tag remove sel 1.0 end + if {$match ne {}} { + $ctext see $match + $ctext tag add sel $match "$match + $mlen c" + } + } +} + +method _mark_range {first last} { + set mend $first.0 + while {1} { + set match [_do_search $this $mend mlen -forwards $last.end] + if {$match eq {}} break + set mend "$match + $mlen c" + $ctext tag add found $match $mend + } +} + +method _set_marks {doall} { + set topline [lindex [split [$ctext index @0,0] .] 0] + set botline [lindex [split [$ctext index @0,[winfo height $ctext]] .] 0] + if {$doall || $botline < $smarktop || $topline > $smarkbot} { + # no overlap with previous + _mark_range $this $topline $botline + set smarktop $topline + set smarkbot $botline + } else { + if {$topline < $smarktop} { + _mark_range $this $topline [expr {$smarktop-1}] + set smarktop $topline + } + if {$botline > $smarkbot} { + _mark_range $this [expr {$smarkbot+1}] $botline + set smarkbot $botline + } + } +} + +method scrolled {} { + if {$searchstring ne {}} { + after idle [cb _set_marks 0] + } +} + +} diff --git a/git-gui/lib/shortcut.tcl b/git-gui/lib/shortcut.tcl new file mode 100644 index 0000000..97d1d7a --- /dev/null +++ b/git-gui/lib/shortcut.tcl @@ -0,0 +1,143 @@ +# git-gui desktop icon creators +# Copyright (C) 2006, 2007 Shawn Pearce + +proc do_windows_shortcut {} { + global _gitworktree + set fn [tk_getSaveFile \ + -parent . \ + -title [mc "%s (%s): Create Desktop Icon" [appname] [reponame]] \ + -initialfile "Git [reponame].lnk"] + if {$fn != {}} { + if {[file extension $fn] ne {.lnk}} { + set fn ${fn}.lnk + } + # Use git-gui.exe if available (ie: git-for-windows) + set cmdLine [auto_execok git-gui.exe] + if {$cmdLine eq {}} { + set cmdLine [list [info nameofexecutable] \ + [file normalize $::argv0]] + } + if {[catch { + win32_create_lnk $fn $cmdLine \ + [file normalize $_gitworktree] + } err]} { + error_popup [strcat [mc "Cannot write shortcut:"] "\n\n$err"] + } + } +} + +proc do_cygwin_shortcut {} { + global argv0 _gitworktree + + if {[catch { + set desktop [exec cygpath \ + --windows \ + --absolute \ + --long-name \ + --desktop] + }]} { + set desktop . + } + set fn [tk_getSaveFile \ + -parent . \ + -title [mc "%s (%s): Create Desktop Icon" [appname] [reponame]] \ + -initialdir $desktop \ + -initialfile "Git [reponame].lnk"] + if {$fn != {}} { + if {[file extension $fn] ne {.lnk}} { + set fn ${fn}.lnk + } + if {[catch { + set sh [exec cygpath \ + --windows \ + --absolute \ + /bin/sh.exe] + set me [exec cygpath \ + --unix \ + --absolute \ + $argv0] + win32_create_lnk $fn [list \ + $sh -c \ + "CHERE_INVOKING=1 source /etc/profile;[sq $me] &" \ + ] \ + [file normalize $_gitworktree] + } err]} { + error_popup [strcat [mc "Cannot write shortcut:"] "\n\n$err"] + } + } +} + +proc do_macosx_app {} { + global argv0 env + + set fn [tk_getSaveFile \ + -parent . \ + -title [mc "%s (%s): Create Desktop Icon" [appname] [reponame]] \ + -initialdir [file join $env(HOME) Desktop] \ + -initialfile "Git [reponame].app"] + if {$fn != {}} { + if {[file extension $fn] ne {.app}} { + set fn ${fn}.app + } + if {[catch { + set Contents [file join $fn Contents] + set MacOS [file join $Contents MacOS] + set exe [file join $MacOS git-gui] + + file mkdir $MacOS + + set fd [open [file join $Contents Info.plist] w] + puts $fd {<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleExecutable</key> + <string>git-gui</string> + <key>CFBundleIdentifier</key> + <string>org.spearce.git-gui</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>1.0</string> + <key>NSPrincipalClass</key> + <string>NSApplication</string> +</dict> +</plist>} + close $fd + + set fd [open $exe w] + puts $fd "#!/bin/sh" + foreach name [lsort [array names env]] { + set value $env($name) + switch -- $name { + GIT_DIR { set value [file normalize [gitdir]] } + } + + switch -glob -- $name { + SSH_* - + GIT_* { + puts $fd "if test \"z\$$name\" = z; then" + puts $fd " export $name=[sq $value]" + puts $fd "fi &&" + } + } + } + puts $fd "export PATH=[sq [file dirname $::_git]]:\$PATH &&" + puts $fd "cd [sq [file normalize [pwd]]] &&" + puts $fd "exec \\" + puts $fd " [sq [info nameofexecutable]] \\" + puts $fd " [sq [file normalize $argv0]]" + close $fd + + file attributes $exe -permissions u+x,g+x,o+x + } err]} { + error_popup [strcat [mc "Cannot write icon:"] "\n\n$err"] + } + } +} diff --git a/git-gui/lib/spellcheck.tcl b/git-gui/lib/spellcheck.tcl new file mode 100644 index 0000000..538d61c --- /dev/null +++ b/git-gui/lib/spellcheck.tcl @@ -0,0 +1,415 @@ +# git-gui spellchecking support through ispell/aspell +# Copyright (C) 2008 Shawn Pearce + +class spellcheck { + +field s_fd {} ; # pipe to ispell/aspell +field s_version {} ; # ispell/aspell version string +field s_lang {} ; # current language code +field s_prog aspell; # are we actually old ispell? +field s_failed 0 ; # is $s_prog bogus and not working? + +field w_text ; # text widget we are spelling +field w_menu ; # context menu for the widget +field s_menuidx 0 ; # last index of insertion into $w_menu + +field s_i {} ; # timer registration for _run callbacks +field s_clear 0 ; # did we erase misspelled tags yet? +field s_seen [list] ; # lines last seen from $w_text in _run +field s_checked [list] ; # lines already checked +field s_pending [list] ; # [$line $data] sent to ispell/aspell +field s_suggest ; # array, list of suggestions, keyed by misspelling + +constructor init {pipe_fd ui_text ui_menu} { + set w_text $ui_text + set w_menu $ui_menu + array unset s_suggest + + bind_button3 $w_text [cb _popup_suggest %X %Y @%x,%y] + _connect $this $pipe_fd + return $this +} + +method _connect {pipe_fd} { + fconfigure $pipe_fd \ + -encoding utf-8 \ + -eofchar {} \ + -translation lf + + if {[gets $pipe_fd s_version] <= 0} { + if {[catch {close $pipe_fd} err]} { + + # Eh? Is this actually ispell choking on aspell options? + # + if {$s_prog eq {aspell} + && [regexp -nocase {^Usage: } $err] + && ![catch { + set pipe_fd [open [list | $s_prog -v] r] + gets $pipe_fd s_version + close $pipe_fd + }] + && $s_version ne {}} { + if {{@(#) } eq [string range $s_version 0 4]} { + set s_version [string range $s_version 5 end] + } + set s_failed 1 + error_popup [strcat \ + [mc "Unsupported spell checker"] \ + ":\n\n$s_version"] + set s_version {} + return + } + + regsub -nocase {^Error: } $err {} err + if {$s_fd eq {}} { + error_popup [strcat [mc "Spell checking is unavailable"] ":\n\n$err"] + } else { + error_popup [strcat \ + [mc "Invalid spell checking configuration"] \ + ":\n\n$err\n\n" \ + [mc "Reverting dictionary to %s." $s_lang]] + } + } else { + error_popup [mc "Spell checker silently failed on startup"] + } + return + } + + if {{@(#) } ne [string range $s_version 0 4]} { + catch {close $pipe_fd} + error_popup [strcat [mc "Unrecognized spell checker"] ":\n\n$s_version"] + return + } + set s_version [string range [string trim $s_version] 5 end] + regexp \ + {International Ispell Version .* \(but really (Aspell .*?)\)$} \ + $s_version _junk s_version + regexp {^Aspell (\d)+\.(\d+)} $s_version _junk major minor + + puts $pipe_fd ! ; # enable terse mode + + # fetch the language + if {$major > 0 || ($major == 0 && $minor >= 60)} { + puts $pipe_fd {$$cr master} + flush $pipe_fd + gets $pipe_fd s_lang + regexp {[/\\]([^/\\]+)\.[^\.]+$} $s_lang _ s_lang + } else { + set s_lang {} + } + + if {$::default_config(gui.spellingdictionary) eq {} + && [get_config gui.spellingdictionary] eq {}} { + set ::default_config(gui.spellingdictionary) $s_lang + } + + if {$s_fd ne {}} { + catch {close $s_fd} + } + set s_fd $pipe_fd + + fconfigure $s_fd -blocking 0 + fileevent $s_fd readable [cb _read] + + $w_text tag conf misspelled \ + -foreground red \ + -underline 1 + + array unset s_suggest + set s_seen [list] + set s_checked [list] + set s_pending [list] + _run $this +} + +method lang {{n {}}} { + if {$n ne {} && $s_lang ne $n && !$s_failed} { + set spell_cmd [list |] + lappend spell_cmd aspell + lappend spell_cmd --master=$n + lappend spell_cmd --mode=none + lappend spell_cmd --encoding=UTF-8 + lappend spell_cmd pipe + _connect $this [open $spell_cmd r+] + } + return $s_lang +} + +method version {} { + if {$s_version ne {}} { + return "$s_version, $s_lang" + } + return {} +} + +method stop {} { + while {$s_menuidx > 0} { + $w_menu delete 0 + incr s_menuidx -1 + } + $w_text tag delete misspelled + + catch {close $s_fd} + catch {after cancel $s_i} + set s_fd {} + set s_i {} + set s_lang {} +} + +method _popup_suggest {X Y pos} { + while {$s_menuidx > 0} { + $w_menu delete 0 + incr s_menuidx -1 + } + + set b_loc [$w_text index "$pos wordstart"] + set e_loc [_wordend $this $b_loc] + set orig [$w_text get $b_loc $e_loc] + set tags [$w_text tag names $b_loc] + + if {[lsearch -exact $tags misspelled] >= 0} { + if {[info exists s_suggest($orig)]} { + set cnt 0 + foreach s $s_suggest($orig) { + if {$cnt < 5} { + $w_menu insert $s_menuidx command \ + -label $s \ + -command [cb _replace $b_loc $e_loc $s] + incr s_menuidx + incr cnt + } else { + break + } + } + } else { + $w_menu insert $s_menuidx command \ + -label [mc "No Suggestions"] \ + -state disabled + incr s_menuidx + } + $w_menu insert $s_menuidx separator + incr s_menuidx + } + + $w_text mark set saved-insert insert + tk_popup $w_menu $X $Y +} + +method _replace {b_loc e_loc word} { + $w_text configure -autoseparators 0 + $w_text edit separator + + $w_text delete $b_loc $e_loc + $w_text insert $b_loc $word + + $w_text edit separator + $w_text configure -autoseparators 1 + $w_text mark set insert saved-insert +} + +method _restart_timer {} { + set s_i [after 300 [cb _run]] +} + +proc _match_length {max_line arr_name} { + upvar $arr_name a + + if {[llength $a] > $max_line} { + set a [lrange $a 0 $max_line] + } + while {[llength $a] <= $max_line} { + lappend a {} + } +} + +method _wordend {pos} { + set pos [$w_text index "$pos wordend"] + set tags [$w_text tag names $pos] + while {[lsearch -exact $tags misspelled] >= 0} { + set pos [$w_text index "$pos +1c"] + set tags [$w_text tag names $pos] + } + return $pos +} + +method _run {} { + set cur_pos [$w_text index {insert -1c}] + set cur_line [lindex [split $cur_pos .] 0] + set max_line [lindex [split [$w_text index end] .] 0] + _match_length $max_line s_seen + _match_length $max_line s_checked + + # Nothing in the message buffer? Nothing to spellcheck. + # + if {$cur_line == 1 + && $max_line == 2 + && [$w_text get 1.0 end] eq "\n"} { + array unset s_suggest + _restart_timer $this + return + } + + set active 0 + for {set n 1} {$n <= $max_line} {incr n} { + set s [$w_text get "$n.0" "$n.end"] + + # Don't spellcheck the current line unless we are at + # a word boundary. The user might be typing on it. + # + if {$n == $cur_line + && ![regexp {^\W$} [$w_text get $cur_pos insert]]} { + + # If the current word is misspelled remove the tag + # but force a spellcheck later. + # + set tags [$w_text tag names $cur_pos] + if {[lsearch -exact $tags misspelled] >= 0} { + $w_text tag remove misspelled \ + "$cur_pos wordstart" \ + [_wordend $this $cur_pos] + lset s_seen $n $s + lset s_checked $n {} + } + + continue + } + + if {[lindex $s_seen $n] eq $s + && [lindex $s_checked $n] ne $s} { + # Don't send empty lines to Aspell it doesn't check them. + # + if {$s eq {}} { + lset s_checked $n $s + continue + } + + # Don't send typical s-b-o lines as the emails are + # almost always misspelled according to Aspell. + # + if {[regexp -nocase {^[a-z-]+-by:.*<.*@.*>$} $s]} { + $w_text tag remove misspelled "$n.0" "$n.end" + lset s_checked $n $s + continue + } + + puts $s_fd ^$s + lappend s_pending [list $n $s] + set active 1 + } else { + # Delay until another idle loop to make sure we don't + # spellcheck lines the user is actively changing. + # + lset s_seen $n $s + } + } + + if {$active} { + set s_clear 1 + flush $s_fd + } else { + _restart_timer $this + } +} + +method _read {} { + while {[gets $s_fd line] >= 0} { + set lineno [lindex $s_pending 0 0] + set line [string trim $line] + + if {$s_clear} { + $w_text tag remove misspelled "$lineno.0" "$lineno.end" + set s_clear 0 + } + + if {$line eq {}} { + lset s_checked $lineno [lindex $s_pending 0 1] + set s_pending [lrange $s_pending 1 end] + set s_clear 1 + continue + } + + set sugg [list] + switch -- [string range $line 0 1] { + {& } { + set line [split [string range $line 2 end] :] + set info [split [lindex $line 0] { }] + set orig [lindex $info 0] + set offs [lindex $info 2] + foreach s [split [lindex $line 1] ,] { + lappend sugg [string range $s 1 end] + } + } + {# } { + set info [split [string range $line 2 end] { }] + set orig [lindex $info 0] + set offs [lindex $info 1] + } + default { + puts stderr "<spell> $line" + continue + } + } + + incr offs -1 + set b_loc "$lineno.$offs" + set e_loc [$w_text index "$lineno.$offs wordend"] + set curr [$w_text get $b_loc $e_loc] + + # At least for English curr = "bob", orig = "bob's" + # so Tk didn't include the 's but Aspell did. We + # try to round out the word. + # + while {$curr ne $orig + && [string equal -length [string length $curr] $curr $orig]} { + set n_loc [$w_text index "$e_loc +1c"] + set n_curr [$w_text get $b_loc $n_loc] + if {$n_curr eq $curr} { + break + } + set curr $n_curr + set e_loc $n_loc + } + + if {$curr eq $orig} { + $w_text tag add misspelled $b_loc $e_loc + if {[llength $sugg] > 0} { + set s_suggest($orig) $sugg + } else { + unset -nocomplain s_suggest($orig) + } + } else { + unset -nocomplain s_suggest($orig) + } + } + + fconfigure $s_fd -block 1 + if {[eof $s_fd]} { + if {![catch {close $s_fd} err]} { + set err [mc "Unexpected EOF from spell checker"] + } + catch {after cancel $s_i} + $w_text tag remove misspelled 1.0 end + error_popup [strcat [mc "Spell Checker Failed"] "\n\n" $err] + return + } + fconfigure $s_fd -block 0 + + if {[llength $s_pending] == 0} { + _restart_timer $this + } +} + +proc available_langs {} { + set langs [list] + catch { + set fd [open [list | aspell dump dicts] r] + while {[gets $fd line] >= 0} { + if {$line eq {}} continue + lappend langs $line + } + close $fd + } + return $langs +} + +} diff --git a/git-gui/lib/sshkey.tcl b/git-gui/lib/sshkey.tcl new file mode 100644 index 0000000..589ff8f --- /dev/null +++ b/git-gui/lib/sshkey.tcl @@ -0,0 +1,131 @@ +# git-gui about git-gui dialog +# Copyright (C) 2006, 2007 Shawn Pearce + +proc find_ssh_key {} { + foreach name { + ~/.ssh/id_dsa.pub ~/.ssh/id_ecdsa.pub ~/.ssh/id_ed25519.pub + ~/.ssh/id_rsa.pub ~/.ssh/identity.pub + } { + if {[file exists $name]} { + set fh [open $name r] + set cont [read $fh] + close $fh + return [list $name $cont] + } + } + + return {} +} + +proc do_ssh_key {} { + global sshkey_title have_tk85 sshkey_fd use_ttk NS + + set w .sshkey_dialog + if {[winfo exists $w]} { + raise $w + return + } + + Dialog $w + wm transient $w . + + set finfo [find_ssh_key] + if {$finfo eq {}} { + set sshkey_title [mc "No keys found."] + set gen_state normal + } else { + set sshkey_title [mc "Found a public key in: %s" [lindex $finfo 0]] + set gen_state disabled + } + + ${NS}::frame $w.header + ${NS}::label $w.header.lbl -textvariable sshkey_title -anchor w + ${NS}::button $w.header.gen -text [mc "Generate Key"] \ + -command [list make_ssh_key $w] -state $gen_state + pack $w.header.lbl -side left -expand 1 -fill x + pack $w.header.gen -side right + pack $w.header -fill x -pady 5 -padx 5 + + text $w.contents -width 60 -height 10 -wrap char -relief sunken + pack $w.contents -fill both -expand 1 + if {$have_tk85} { + set clr darkblue + if {$use_ttk} { set clr [ttk::style lookup . -selectbackground] } + $w.contents configure -inactiveselectbackground $clr + } + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.close -text [mc Close] \ + -default active -command [list destroy $w] + pack $w.buttons.close -side right + ${NS}::button $w.buttons.copy -text [mc "Copy To Clipboard"] \ + -command [list tk_textCopy $w.contents] + pack $w.buttons.copy -side left + pack $w.buttons -side bottom -fill x -pady 5 -padx 5 + + if {$finfo ne {}} { + $w.contents insert end [lindex $finfo 1] sel + } + $w.contents configure -state disabled + + bind $w <Visibility> "grab $w; focus $w.buttons.close" + bind $w <Key-Escape> "destroy $w" + bind $w <Key-Return> "destroy $w" + bind $w <Destroy> kill_sshkey + wm title $w [mc "Your OpenSSH Public Key"] + tk::PlaceWindow $w widget . + tkwait window $w +} + +proc make_ssh_key {w} { + global sshkey_title sshkey_output sshkey_fd + + set sshkey_title [mc "Generating..."] + $w.header.gen configure -state disabled + + set cmdline [list sh -c {echo | ssh-keygen -q -t rsa -f ~/.ssh/id_rsa 2>&1}] + + if {[catch { set sshkey_fd [_open_stdout_stderr $cmdline] } err]} { + error_popup [mc "Could not start ssh-keygen:\n\n%s" $err] + return + } + + set sshkey_output {} + fconfigure $sshkey_fd -blocking 0 + fileevent $sshkey_fd readable [list read_sshkey_output $sshkey_fd $w] +} + +proc kill_sshkey {} { + global sshkey_fd + if {![info exists sshkey_fd]} return + catch { kill_file_process $sshkey_fd } + catch { close $sshkey_fd } +} + +proc read_sshkey_output {fd w} { + global sshkey_fd sshkey_output sshkey_title + + set sshkey_output "$sshkey_output[read $fd]" + if {![eof $fd]} return + + fconfigure $fd -blocking 1 + unset sshkey_fd + + $w.contents configure -state normal + if {[catch {close $fd} err]} { + set sshkey_title [mc "Generation failed."] + $w.contents insert end $err + $w.contents insert end "\n" + $w.contents insert end $sshkey_output + } else { + set finfo [find_ssh_key] + if {$finfo eq {}} { + set sshkey_title [mc "Generation succeeded, but no keys found."] + $w.contents insert end $sshkey_output + } else { + set sshkey_title [mc "Your key is in: %s" [lindex $finfo 0]] + $w.contents insert end [lindex $finfo 1] sel + } + } + $w.contents configure -state disable +} diff --git a/git-gui/lib/status_bar.tcl b/git-gui/lib/status_bar.tcl new file mode 100644 index 0000000..d32b141 --- /dev/null +++ b/git-gui/lib/status_bar.tcl @@ -0,0 +1,312 @@ +# git-gui status bar mega-widget +# Copyright (C) 2007 Shawn Pearce + +# The status_bar class manages the entire status bar. It is possible for +# multiple overlapping asynchronous operations to want to display status +# simultaneously. Each one receives a status_bar_operation when it calls the +# start method, and the status bar combines all active operations into the +# line of text it displays. Most of the time, there will be at most one +# ongoing operation. +# +# Note that the entire status bar can be either in single-line or two-line +# mode, depending on the constructor. Multiple active operations are only +# supported for single-line status bars. + +class status_bar { + +field allow_multiple ; # configured at construction + +field w ; # our own window path +field w_l ; # text widget we draw messages into +field w_c ; # canvas we draw a progress bar into +field c_pack ; # script to pack the canvas with + +field baseline_text ; # text to show if there are no operations +field status_bar_text ; # combined text for all operations + +field operations ; # list of current ongoing operations + +# The status bar can display a progress bar, updated when consumers call the +# update method on their status_bar_operation. When there are multiple +# operations, the status bar shows the combined status of all operations. +# +# When an overlapping operation completes, the progress bar is going to +# abruptly have one fewer operation in the calculation, causing a discontinuity. +# Therefore, whenever an operation completes, if it is not the last operation, +# this counter is increased, and the progress bar is calculated as though there +# were still another operation at 100%. When the last operation completes, this +# is reset to 0. +field completed_operation_count + +constructor new {path} { + global use_ttk NS + set w $path + set w_l $w.l + set w_c $w.c + + # Standard single-line status bar: Permit overlapping operations + set allow_multiple 1 + + set baseline_text "" + set operations [list] + set completed_operation_count 0 + + ${NS}::frame $w + if {!$use_ttk} { + $w configure -borderwidth 1 -relief sunken + } + ${NS}::label $w_l \ + -textvariable @status_bar_text \ + -anchor w \ + -justify left + pack $w_l -side left + set c_pack [cb _oneline_pack] + + bind $w <Destroy> [cb _delete %W] + return $this +} + +method _oneline_pack {} { + $w_c conf -width 100 + pack $w_c -side right +} + +constructor two_line {path} { + global NS + set w $path + set w_l $w.l + set w_c $w.c + + # Two-line status bar: Only one ongoing operation permitted. + set allow_multiple 0 + + set baseline_text "" + set operations [list] + set completed_operation_count 0 + + ${NS}::frame $w + ${NS}::label $w_l \ + -textvariable @status_bar_text \ + -anchor w \ + -justify left + pack $w_l -anchor w -fill x + set c_pack [list pack $w_c -fill x] + + bind $w <Destroy> [cb _delete %W] + return $this +} + +method ensure_canvas {} { + if {[winfo exists $w_c]} { + $w_c coords bar 0 0 0 20 + } else { + canvas $w_c \ + -height [expr {int([winfo reqheight $w_l] * 0.6)}] \ + -borderwidth 1 \ + -relief groove \ + -highlightt 0 + $w_c create rectangle 0 0 0 20 -tags bar -fill navy + eval $c_pack + } +} + +method show {msg} { + $this ensure_canvas + set baseline_text $msg + $this refresh +} + +method start {msg {uds {}}} { + set baseline_text "" + + if {!$allow_multiple && [llength $operations]} { + return [lindex $operations 0] + } + + $this ensure_canvas + + set operation [status_bar_operation::new $this $msg $uds] + + lappend operations $operation + + $this refresh + + return $operation +} + +method refresh {} { + set new_text "" + + set total [expr $completed_operation_count * 100] + set have $total + + foreach operation $operations { + if {$new_text != ""} { + append new_text " / " + } + + append new_text [$operation get_status] + + set total [expr $total + 100] + set have [expr $have + [$operation get_progress]] + } + + if {$new_text == ""} { + set new_text $baseline_text + } + + set status_bar_text $new_text + + if {[winfo exists $w_c]} { + set pixel_width 0 + if {$have > 0} { + set pixel_width [expr {[winfo width $w_c] * $have / $total}] + } + + $w_c coords bar 0 0 $pixel_width 20 + } +} + +method stop {operation stop_msg} { + set idx [lsearch $operations $operation] + + if {$idx >= 0} { + set operations [lreplace $operations $idx $idx] + set completed_operation_count [expr \ + $completed_operation_count + 1] + + if {[llength $operations] == 0} { + set completed_operation_count 0 + + destroy $w_c + if {$stop_msg ne {}} { + set baseline_text $stop_msg + } + } + + $this refresh + } +} + +method stop_all {{stop_msg {}}} { + # This makes the operation's call to stop a no-op. + set operations_copy $operations + set operations [list] + + foreach operation $operations_copy { + $operation stop + } + + if {$stop_msg ne {}} { + set baseline_text $stop_msg + } + + $this refresh +} + +method _delete {current} { + if {$current eq $w} { + delete_this + } +} + +} + +# The status_bar_operation class tracks a single consumer's ongoing status bar +# activity, with the context that there are a few situations where multiple +# overlapping asynchronous operations might want to display status information +# simultaneously. Instances of status_bar_operation are created by calling +# start on the status_bar, and when the caller is done with its stauts bar +# operation, it calls stop on the operation. + +class status_bar_operation { + +field status_bar; # reference back to the status_bar that owns this object + +field is_active; + +field status {}; # single line of text we show +field progress {}; # current progress (0 to 100) +field prefix {}; # text we format into status +field units {}; # unit of progress +field meter {}; # current core git progress meter (if active) + +constructor new {owner msg uds} { + set status_bar $owner + + set status $msg + set progress 0 + set prefix $msg + set units $uds + set meter {} + + set is_active 1 + + return $this +} + +method get_is_active {} { return $is_active } +method get_status {} { return $status } +method get_progress {} { return $progress } + +method update {have total} { + if {!$is_active} { return } + + set progress 0 + + if {$total > 0} { + set progress [expr {100 * $have / $total}] + } + + set prec [string length [format %i $total]] + + set status [mc "%s ... %*i of %*i %s (%3i%%)" \ + $prefix \ + $prec $have \ + $prec $total \ + $units $progress] + + $status_bar refresh +} + +method update_meter {buf} { + if {!$is_active} { return } + + append meter $buf + set r [string last "\r" $meter] + if {$r == -1} { + return + } + + set prior [string range $meter 0 $r] + set meter [string range $meter [expr {$r + 1}] end] + set p "\\((\\d+)/(\\d+)\\)" + if {[regexp ":\\s*\\d+% $p\(?:, done.\\s*\n|\\s*\r)\$" $prior _j a b]} { + update $this $a $b + } elseif {[regexp "$p\\s+done\r\$" $prior _j a b]} { + update $this $a $b + } +} + +method stop {{stop_msg {}}} { + if {$is_active} { + set is_active 0 + $status_bar stop $this $stop_msg + } +} + +method restart {msg} { + if {!$is_active} { return } + + set status $msg + set prefix $msg + set meter {} + $status_bar refresh +} + +method _delete {} { + stop + delete_this +} + +} diff --git a/git-gui/lib/themed.tcl b/git-gui/lib/themed.tcl new file mode 100644 index 0000000..88b3119 --- /dev/null +++ b/git-gui/lib/themed.tcl @@ -0,0 +1,355 @@ +# Functions for supporting the use of themed Tk widgets in git-gui. +# Copyright (C) 2009 Pat Thoyts <patthoyts@users.sourceforge.net> + +proc ttk_get_current_theme {} { + # Handle either current Tk or older versions of 8.5 + if {[catch {set theme [ttk::style theme use]}]} { + set theme $::ttk::currentTheme + } + return $theme +} + +proc InitTheme {} { + # Create a color label style (bg can be overridden by widget option) + ttk::style layout Color.TLabel { + Color.Label.border -sticky news -children { + Color.label.fill -sticky news -children { + Color.Label.padding -sticky news -children { + Color.Label.label -sticky news}}}} + eval [linsert [ttk::style configure TLabel] 0 \ + ttk::style configure Color.TLabel] + ttk::style configure Color.TLabel \ + -borderwidth 0 -relief flat -padding 2 + ttk::style map Color.TLabel -background {{} gold} + # We also need a padded label. + ttk::style configure Padded.TLabel \ + -padding {5 5} -borderwidth 1 -relief solid + # We need a gold frame. + ttk::style layout Gold.TFrame { + Gold.Frame.border -sticky nswe -children { + Gold.Frame.fill -sticky nswe}} + ttk::style configure Gold.TFrame -background gold -relief flat + # listboxes should have a theme border so embed in ttk::frame + ttk::style layout SListbox.TFrame { + SListbox.Frame.Entry.field -sticky news -border true -children { + SListbox.Frame.padding -sticky news + } + } + + set theme [ttk_get_current_theme] + + if {[lsearch -exact {default alt classic clam} $theme] != -1} { + # Simple override of standard ttk::entry to change the field + # packground according to a state flag. We should use 'user1' + # but not all versions of 8.5 support that so make use of 'pressed' + # which is not normally in use for entry widgets. + ttk::style layout Edged.Entry [ttk::style layout TEntry] + ttk::style map Edged.Entry {*}[ttk::style map TEntry] + ttk::style configure Edged.Entry {*}[ttk::style configure TEntry] \ + -fieldbackground lightgreen + ttk::style map Edged.Entry -fieldbackground { + {pressed !disabled} lightpink + } + } else { + # For fancier themes, in particular the Windows ones, the field + # element may not support changing the background color. So instead + # override the fill using the default fill element. If we overrode + # the vista theme field element we would loose the themed border + # of the widget. + catch { + ttk::style element create color.fill from default + } + + ttk::style layout Edged.Entry { + Edged.Entry.field -sticky nswe -border 0 -children { + Edged.Entry.border -sticky nswe -border 1 -children { + Edged.Entry.padding -sticky nswe -children { + Edged.Entry.color.fill -sticky nswe -children { + Edged.Entry.textarea -sticky nswe + } + } + } + } + } + + ttk::style configure Edged.Entry {*}[ttk::style configure TEntry] \ + -background lightgreen -padding 0 -borderwidth 0 + ttk::style map Edged.Entry {*}[ttk::style map TEntry] \ + -background {{pressed !disabled} lightpink} + } + + if {[lsearch [bind . <<ThemeChanged>>] InitTheme] == -1} { + bind . <<ThemeChanged>> +[namespace code [list InitTheme]] + } +} + +# Define a style used for the surround of text widgets. +proc InitEntryFrame {} { + ttk::style theme settings default { + ttk::style layout EntryFrame { + EntryFrame.field -sticky nswe -border 0 -children { + EntryFrame.fill -sticky nswe -children { + EntryFrame.padding -sticky nswe + } + } + } + ttk::style configure EntryFrame -padding 1 -relief sunken + ttk::style map EntryFrame -background {} + } + ttk::style theme settings classic { + ttk::style configure EntryFrame -padding 2 -relief sunken + ttk::style map EntryFrame -background {} + } + ttk::style theme settings alt { + ttk::style configure EntryFrame -padding 2 + ttk::style map EntryFrame -background {} + } + ttk::style theme settings clam { + ttk::style configure EntryFrame -padding 2 + ttk::style map EntryFrame -background {} + } + + # Ignore errors for missing native themes + catch { + ttk::style theme settings winnative { + ttk::style configure EntryFrame -padding 2 + } + ttk::style theme settings xpnative { + ttk::style configure EntryFrame -padding 1 + ttk::style element create EntryFrame.field vsapi \ + EDIT 1 {disabled 4 focus 3 active 2 {} 1} -padding 1 + } + ttk::style theme settings vista { + ttk::style configure EntryFrame -padding 2 + ttk::style element create EntryFrame.field vsapi \ + EDIT 6 {disabled 4 focus 3 active 2 {} 1} -padding 2 + } + } + + bind EntryFrame <Enter> {%W instate !disabled {%W state active}} + bind EntryFrame <Leave> {%W state !active} + bind EntryFrame <<ThemeChanged>> { + set pad [ttk::style lookup EntryFrame -padding] + %W configure -padding [expr {$pad eq {} ? 1 : $pad}] + } +} + +proc gold_frame {w args} { + global use_ttk + if {$use_ttk} { + eval [linsert $args 0 ttk::frame $w -style Gold.TFrame] + } else { + eval [linsert $args 0 frame $w -background gold] + } +} + +proc tlabel {w args} { + global use_ttk + if {$use_ttk} { + set cmd [list ttk::label $w -style Color.TLabel] + foreach {k v} $args { + switch -glob -- $k { + -activebackground {} + default { lappend cmd $k $v } + } + } + eval $cmd + } else { + eval [linsert $args 0 label $w] + } +} + +# The padded label gets used in the about class. +proc paddedlabel {w args} { + global use_ttk + if {$use_ttk} { + eval [linsert $args 0 ttk::label $w -style Padded.TLabel] + } else { + eval [linsert $args 0 label $w \ + -padx 5 -pady 5 \ + -justify left \ + -anchor w \ + -borderwidth 1 \ + -relief solid] + } +} + +# Create a toplevel for use as a dialog. +# If available, sets the EWMH dialog hint and if ttk is enabled +# place a themed frame over the surface. +proc Dialog {w args} { + eval [linsert $args 0 toplevel $w -class Dialog] + catch {wm attributes $w -type dialog} + pave_toplevel $w + return $w +} + +# Tk toplevels are not themed - so pave it over with a themed frame to get +# the base color correct per theme. +proc pave_toplevel {w} { + global use_ttk + if {$use_ttk && ![winfo exists $w.!paving]} { + set paving [ttk::frame $w.!paving] + place $paving -x 0 -y 0 -relwidth 1 -relheight 1 + lower $paving + } +} + +# Create a scrolled listbox with appropriate border for the current theme. +# On many themes the border for a scrolled listbox needs to go around the +# listbox and the scrollbar. +proc slistbox {w args} { + global use_ttk NS + if {$use_ttk} { + set f [ttk::frame $w -style SListbox.TFrame -padding 2] + } else { + set f [frame $w -relief flat] + } + if {[catch { + if {$use_ttk} { + eval [linsert $args 0 listbox $f.list -relief flat \ + -highlightthickness 0 -borderwidth 0] + } else { + eval [linsert $args 0 listbox $f.list] + } + ${NS}::scrollbar $f.vs -command [list $f.list yview] + $f.list configure -yscrollcommand [list $f.vs set] + grid $f.list $f.vs -sticky news + grid rowconfigure $f 0 -weight 1 + grid columnconfigure $f 0 -weight 1 + bind $f.list <<ListboxSelect>> \ + [list event generate $w <<ListboxSelect>>] + interp hide {} $w + interp alias {} $w {} $f.list + } err]} { + destroy $f + return -code error $err + } + return $w +} + +# fetch the background color from a widget. +proc get_bg_color {w} { + global use_ttk + if {$use_ttk} { + set bg [ttk::style lookup [winfo class $w] -background] + } else { + set bg [$w cget -background] + } + return $bg +} + +# ttk::spinbox didn't get added until 8.6 +proc tspinbox {w args} { + global use_ttk + if {$use_ttk && [llength [info commands ttk::spinbox]] > 0} { + eval [linsert $args 0 ttk::spinbox $w] + } else { + eval [linsert $args 0 spinbox $w] + } +} + +# Create a text widget with any theme specific properties. +proc ttext {w args} { + global use_ttk + if {$use_ttk} { + switch -- [ttk_get_current_theme] { + "vista" - "xpnative" { + lappend args -highlightthickness 0 -borderwidth 0 + } + } + } + set w [eval [linsert $args 0 text $w]] + if {$use_ttk} { + if {[winfo class [winfo parent $w]] eq "EntryFrame"} { + bind $w <FocusIn> {[winfo parent %W] state focus} + bind $w <FocusOut> {[winfo parent %W] state !focus} + } + } + return $w +} + +# themed frame suitable for surrounding a text field. +proc textframe {w args} { + global use_ttk + if {$use_ttk} { + if {[catch {ttk::style layout EntryFrame}]} { + InitEntryFrame + } + eval [linsert $args 0 ttk::frame $w -class EntryFrame -style EntryFrame] + } else { + eval [linsert $args 0 frame $w] + } + return $w +} + +proc tentry {w args} { + global use_ttk + if {$use_ttk} { + InitTheme + ttk::entry $w -style Edged.Entry + } else { + entry $w + } + + rename $w _$w + interp alias {} $w {} tentry_widgetproc $w + eval [linsert $args 0 tentry_widgetproc $w configure] + return $w +} +proc tentry_widgetproc {w cmd args} { + global use_ttk + switch -- $cmd { + state { + if {$use_ttk} { + return [uplevel 1 [list _$w $cmd] $args] + } else { + if {[lsearch -exact $args pressed] != -1} { + _$w configure -background lightpink + } else { + _$w configure -background lightgreen + } + } + } + configure { + if {$use_ttk} { + if {[set n [lsearch -exact $args -background]] != -1} { + set args [lreplace $args $n [incr n]] + if {[llength $args] == 0} {return} + } + } + return [uplevel 1 [list _$w $cmd] $args] + } + default { return [uplevel 1 [list _$w $cmd] $args] } + } +} + +# Tk 8.6 provides a standard font selection dialog. This uses the native +# dialogs on Windows and MacOSX or a standard Tk dialog on X11. +proc tchoosefont {w title familyvar sizevar} { + if {[package vsatisfies [package provide Tk] 8.6]} { + upvar #0 $familyvar family + upvar #0 $sizevar size + tk fontchooser configure -parent $w -title $title \ + -font [list $family $size] \ + -command [list on_choosefont $familyvar $sizevar] + tk fontchooser show + } else { + choose_font::pick $w $title $familyvar $sizevar + } +} + +# Called when the Tk 8.6 fontchooser selects a font. +proc on_choosefont {familyvar sizevar font} { + upvar #0 $familyvar family + upvar #0 $sizevar size + set font [font actual $font] + set family [dict get $font -family] + set size [dict get $font -size] +} + +# Local variables: +# mode: tcl +# indent-tabs-mode: t +# tab-width: 4 +# End: diff --git a/git-gui/lib/tools.tcl b/git-gui/lib/tools.tcl new file mode 100644 index 0000000..413f1a1 --- /dev/null +++ b/git-gui/lib/tools.tcl @@ -0,0 +1,168 @@ +# git-gui Tools menu implementation + +proc tools_list {} { + global repo_config + + set names {} + foreach item [array names repo_config guitool.*.cmd] { + lappend names [string range $item 8 end-4] + } + return [lsort $names] +} + +proc tools_populate_all {} { + global tools_menubar tools_menutbl + global tools_tailcnt + + set mbar_end [$tools_menubar index end] + set mbar_base [expr {$mbar_end - $tools_tailcnt}] + if {$mbar_base >= 0} { + $tools_menubar delete 0 $mbar_base + } + + array unset tools_menutbl + + foreach fullname [tools_list] { + tools_populate_one $fullname + } +} + +proc tools_create_item {parent args} { + global tools_menubar tools_tailcnt + if {$parent eq $tools_menubar} { + set pos [expr {[$parent index end]-$tools_tailcnt+1}] + eval [list $parent insert $pos] $args + } else { + eval [list $parent add] $args + } +} + +proc tools_populate_one {fullname} { + global tools_menubar tools_menutbl tools_id + + if {![info exists tools_id]} { + set tools_id 0 + } + + set names [split $fullname '/'] + set parent $tools_menubar + for {set i 0} {$i < [llength $names]-1} {incr i} { + set subname [join [lrange $names 0 $i] '/'] + if {[info exists tools_menutbl($subname)]} { + set parent $tools_menutbl($subname) + } else { + set subid $parent.t$tools_id + tools_create_item $parent cascade \ + -label [lindex $names $i] -menu $subid + menu $subid + set tools_menutbl($subname) $subid + set parent $subid + incr tools_id + } + } + + tools_create_item $parent command \ + -label [lindex $names end] \ + -command [list tools_exec $fullname] +} + +proc tools_exec {fullname} { + global repo_config env current_diff_path + global current_branch is_detached + global selected_paths + + if {[is_config_true "guitool.$fullname.needsfile"]} { + if {$current_diff_path eq {}} { + error_popup [mc "Running %s requires a selected file." $fullname] + return + } + } + + catch { unset env(ARGS) } + catch { unset env(REVISION) } + + if {[get_config "guitool.$fullname.revprompt"] ne {} || + [get_config "guitool.$fullname.argprompt"] ne {}} { + set dlg [tools_askdlg::dialog $fullname] + if {![tools_askdlg::execute $dlg]} { + return + } + } elseif {[is_config_true "guitool.$fullname.confirm"]} { + if {[is_config_true "guitool.$fullname.needsfile"]} { + if {[ask_popup [mc "Are you sure you want to run %1\$s on file \"%2\$s\"?" $fullname $current_diff_path]] ne {yes}} { + return + } + } else { + if {[ask_popup [mc "Are you sure you want to run %s?" $fullname]] ne {yes}} { + return + } + } + } + + set env(GIT_GUITOOL) $fullname + set env(FILENAME) $current_diff_path + set env(FILENAMES) [join [array names selected_paths] \n] + if {$is_detached} { + set env(CUR_BRANCH) "" + } else { + set env(CUR_BRANCH) $current_branch + } + + set cmdline $repo_config(guitool.$fullname.cmd) + if {[is_config_true "guitool.$fullname.noconsole"]} { + tools_run_silent [list sh -c $cmdline] \ + [list tools_complete $fullname {}] + } else { + regsub {/} $fullname { / } title + set w [console::new \ + [mc "Tool: %s" $title] \ + [mc "Running: %s" $cmdline]] + console::exec $w [list sh -c $cmdline] \ + [list tools_complete $fullname $w] + } + + unset env(GIT_GUITOOL) + unset env(FILENAME) + unset env(FILENAMES) + unset env(CUR_BRANCH) + catch { unset env(ARGS) } + catch { unset env(REVISION) } +} + +proc tools_run_silent {cmd after} { + lappend cmd 2>@1 + set fd [_open_stdout_stderr $cmd] + + fconfigure $fd -blocking 0 -translation binary + fileevent $fd readable [list tools_consume_input $fd $after] +} + +proc tools_consume_input {fd after} { + read $fd + if {[eof $fd]} { + fconfigure $fd -blocking 1 + if {[catch {close $fd}]} { + uplevel #0 $after 0 + } else { + uplevel #0 $after 1 + } + } +} + +proc tools_complete {fullname w {ok 1}} { + if {$w ne {}} { + console::done $w $ok + } + + if {$ok} { + set msg [mc "Tool completed successfully: %s" $fullname] + } else { + set msg [mc "Tool failed: %s" $fullname] + } + + if {[is_config_true "guitool.$fullname.norescan"]} { + ui_status $msg + } else { + rescan [list ui_status $msg] + } +} diff --git a/git-gui/lib/tools_dlg.tcl b/git-gui/lib/tools_dlg.tcl new file mode 100644 index 0000000..c05413c --- /dev/null +++ b/git-gui/lib/tools_dlg.tcl @@ -0,0 +1,414 @@ +# git-gui Tools menu dialogs + +class tools_add { + +field w ; # widget path +field w_name ; # new remote name widget +field w_cmd ; # new remote location widget + +field name {}; # name of the tool +field command {}; # command to execute +field add_global 0; # add to the --global config +field no_console 0; # disable using the console +field needs_file 0; # ensure filename is set +field confirm 0; # ask for confirmation +field ask_branch 0; # ask for a revision +field ask_args 0; # ask for additional args + +constructor dialog {} { + global repo_config use_ttk NS + + make_dialog top w + wm title $top [mc "%s (%s): Add Tool" [appname] [reponame]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + wm transient $top . + } + + ${NS}::label $w.header -text [mc "Add New Tool Command"] \ + -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::checkbutton $w.buttons.global \ + -text [mc "Add globally"] \ + -variable @add_global + pack $w.buttons.global -side left -padx 5 + ${NS}::button $w.buttons.create -text [mc Add] \ + -default active \ + -command [cb _add] + pack $w.buttons.create -side right + ${NS}::button $w.buttons.cancel -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::labelframe $w.desc -text [mc "Tool Details"] + + ${NS}::label $w.desc.name_cmnt -anchor w\ + -text [mc "Use '/' separators to create a submenu tree:"] + grid x $w.desc.name_cmnt -sticky we -padx {0 5} -pady {0 2} + ${NS}::label $w.desc.name_l -text [mc "Name:"] + set w_name $w.desc.name_t + ${NS}::entry $w_name \ + -width 40 \ + -textvariable @name \ + -validate key \ + -validatecommand [cb _validate_name %d %S] + grid $w.desc.name_l $w_name -sticky we -padx {0 5} + + ${NS}::label $w.desc.cmd_l -text [mc "Command:"] + set w_cmd $w.desc.cmd_t + ${NS}::entry $w_cmd \ + -width 40 \ + -textvariable @command + grid $w.desc.cmd_l $w_cmd -sticky we -padx {0 5} -pady {0 3} + + grid columnconfigure $w.desc 1 -weight 1 + pack $w.desc -anchor nw -fill x -pady 5 -padx 5 + + ${NS}::checkbutton $w.confirm \ + -text [mc "Show a dialog before running"] \ + -variable @confirm -command [cb _check_enable_dlg] + + ${NS}::labelframe $w.dlg -labelwidget $w.confirm + + ${NS}::checkbutton $w.dlg.askbranch \ + -text [mc "Ask the user to select a revision (sets \$REVISION)"] \ + -variable @ask_branch -state disabled + pack $w.dlg.askbranch -anchor w -padx 15 + + ${NS}::checkbutton $w.dlg.askargs \ + -text [mc "Ask the user for additional arguments (sets \$ARGS)"] \ + -variable @ask_args -state disabled + pack $w.dlg.askargs -anchor w -padx 15 + + pack $w.dlg -anchor nw -fill x -pady {0 8} -padx 5 + + ${NS}::checkbutton $w.noconsole \ + -text [mc "Don't show the command output window"] \ + -variable @no_console + pack $w.noconsole -anchor w -padx 5 + + ${NS}::checkbutton $w.needsfile \ + -text [mc "Run only if a diff is selected (\$FILENAME not empty)"] \ + -variable @needs_file + pack $w.needsfile -anchor w -padx 5 + + bind $w <Visibility> [cb _visible] + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _add]\;break + tkwait window $w +} + +method _check_enable_dlg {} { + if {$confirm} { + $w.dlg.askbranch configure -state normal + $w.dlg.askargs configure -state normal + } else { + $w.dlg.askbranch configure -state disabled + $w.dlg.askargs configure -state disabled + } +} + +method _add {} { + global repo_config + + if {$name eq {}} { + error_popup [mc "Please supply a name for the tool."] + focus $w_name + return + } + + set item "guitool.$name.cmd" + + if {[info exists repo_config($item)]} { + error_popup [mc "Tool '%s' already exists." $name] + focus $w_name + return + } + + set cmd [list git config] + if {$add_global} { lappend cmd --global } + set items {} + if {$no_console} { lappend items "guitool.$name.noconsole" } + if {$needs_file} { lappend items "guitool.$name.needsfile" } + if {$confirm} { + if {$ask_args} { lappend items "guitool.$name.argprompt" } + if {$ask_branch} { lappend items "guitool.$name.revprompt" } + if {!$ask_args && !$ask_branch} { + lappend items "guitool.$name.confirm" + } + } + + if {[catch { + eval $cmd [list $item $command] + foreach citem $items { eval $cmd [list $citem yes] } + } err]} { + error_popup [mc "Could not add tool:\n%s" $err] + } else { + set repo_config($item) $command + foreach citem $items { set repo_config($citem) yes } + + tools_populate_all + } + + destroy $w +} + +method _validate_name {d S} { + if {$d == 1} { + if {[regexp {[~?*&\[\0\"\\\{]} $S]} { + return 0 + } + } + return 1 +} + +method _visible {} { + grab $w + $w_name icursor end + focus $w_name +} + +} + +class tools_remove { + +field w ; # widget path +field w_names ; # name list + +constructor dialog {} { + global repo_config global_config system_config use_ttk NS + + load_config 1 + + make_dialog top w + wm title $top [mc "%s (%s): Remove Tool" [appname] [reponame]] + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + wm transient $top . + } + + ${NS}::label $w.header -text [mc "Remove Tool Commands"] \ + -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.create -text [mc Remove] \ + -default active \ + -command [cb _remove] + pack $w.buttons.create -side right + ${NS}::button $w.buttons.cancel -text [mc Cancel] \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::frame $w.list + set w_names $w.list.l + slistbox $w_names \ + -height 10 \ + -width 30 \ + -selectmode extended \ + -exportselection false + pack $w.list.l -side left -fill both -expand 1 + pack $w.list -fill both -expand 1 -pady 5 -padx 5 + + set local_cnt 0 + foreach fullname [tools_list] { + # Cannot delete system tools + if {[info exists system_config(guitool.$fullname.cmd)]} continue + + $w_names insert end $fullname + if {![info exists global_config(guitool.$fullname.cmd)]} { + $w_names itemconfigure end -foreground blue + incr local_cnt + } + } + + if {$local_cnt > 0} { + ${NS}::label $w.colorlbl -foreground blue \ + -text [mc "(Blue denotes repository-local tools)"] + pack $w.colorlbl -fill x -pady 5 -padx 5 + } + + bind $w <Visibility> [cb _visible] + bind $w <Key-Escape> [list destroy $w] + bind $w <Key-Return> [cb _remove]\;break + tkwait window $w +} + +method _remove {} { + foreach i [$w_names curselection] { + set name [$w_names get $i] + + catch { git config --remove-section guitool.$name } + catch { git config --global --remove-section guitool.$name } + } + + load_config 0 + tools_populate_all + + destroy $w +} + +method _visible {} { + grab $w + focus $w_names +} + +} + +class tools_askdlg { + +field w ; # widget path +field w_rev {}; # revision browser +field w_args {}; # arguments + +field is_ask_args 0; # has arguments field +field is_ask_revs 0; # has revision browser + +field is_ok 0; # ok to start +field argstr {}; # arguments + +constructor dialog {fullname} { + global M1B use_ttk NS + + set title [get_config "guitool.$fullname.title"] + if {$title eq {}} { + regsub {/} $fullname { / } title + } + + make_dialog top w -autodelete 0 + wm title $top "[mc "%s (%s):" [appname] [reponame]] $title" + if {$top ne {.}} { + wm geometry $top "+[winfo rootx .]+[winfo rooty .]" + wm transient $top . + } + + set prompt [get_config "guitool.$fullname.prompt"] + if {$prompt eq {}} { + set command [get_config "guitool.$fullname.cmd"] + set prompt [mc "Run Command: %s" $command] + } + + ${NS}::label $w.header -text $prompt -font font_uibold -anchor center + pack $w.header -side top -fill x + + set argprompt [get_config "guitool.$fullname.argprompt"] + set revprompt [get_config "guitool.$fullname.revprompt"] + + set is_ask_args [expr {$argprompt ne {}}] + set is_ask_revs [expr {$revprompt ne {}}] + + if {$is_ask_args} { + if {$argprompt eq {yes} || $argprompt eq {true} || $argprompt eq {1}} { + set argprompt [mc "Arguments"] + } + + ${NS}::labelframe $w.arg -text $argprompt + + set w_args $w.arg.txt + ${NS}::entry $w_args \ + -width 40 \ + -textvariable @argstr + pack $w_args -padx 5 -pady 5 -fill both + pack $w.arg -anchor nw -fill both -pady 5 -padx 5 + } + + if {$is_ask_revs} { + if {$revprompt eq {yes} || $revprompt eq {true} || $revprompt eq {1}} { + set revprompt [mc "Revision"] + } + + if {[is_config_true "guitool.$fullname.revunmerged"]} { + set w_rev [::choose_rev::new_unmerged $w.rev $revprompt] + } else { + set w_rev [::choose_rev::new $w.rev $revprompt] + } + + pack $w.rev -anchor nw -fill both -expand 1 -pady 5 -padx 5 + } + + ${NS}::frame $w.buttons + if {$is_ask_revs} { + ${NS}::button $w.buttons.visualize \ + -text [mc Visualize] \ + -command [cb _visualize] + pack $w.buttons.visualize -side left + } + ${NS}::button $w.buttons.ok \ + -text [mc OK] \ + -command [cb _start] + pack $w.buttons.ok -side right + ${NS}::button $w.buttons.cancel \ + -text [mc "Cancel"] \ + -command [cb _cancel] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + bind $w <$M1B-Key-Return> [cb _start] + bind $w <Key-Return> [cb _start] + bind $w <Key-Escape> [cb _cancel] + wm protocol $w WM_DELETE_WINDOW [cb _cancel] + + bind $w <Visibility> [cb _visible] + return $this +} + +method execute {} { + tkwait window $w + set rv $is_ok + delete_this + return $rv +} + +method _visible {} { + grab $w + if {$is_ask_args} { + focus $w_args + } elseif {$is_ask_revs} { + $w_rev focus_filter + } +} + +method _cancel {} { + wm protocol $w WM_DELETE_WINDOW {} + destroy $w +} + +method _rev {} { + if {[catch {$w_rev commit_or_die}]} { + return {} + } + return [$w_rev get] +} + +method _visualize {} { + global current_branch + set rev [_rev $this] + if {$rev ne {}} { + do_gitk [list --left-right "$current_branch...$rev"] + } +} + +method _start {} { + global env + + if {$is_ask_revs} { + set name [_rev $this] + if {$name eq {}} { + return + } + set env(REVISION) $name + } + + if {$is_ask_args} { + set env(ARGS) $argstr + } + + set is_ok 1 + _cancel $this +} + +} diff --git a/git-gui/lib/transport.tcl b/git-gui/lib/transport.tcl new file mode 100644 index 0000000..a1a424a --- /dev/null +++ b/git-gui/lib/transport.tcl @@ -0,0 +1,232 @@ +# git-gui transport (fetch/push) support +# Copyright (C) 2006, 2007 Shawn Pearce + +proc fetch_from {remote} { + set w [console::new \ + [mc "fetch %s" $remote] \ + [mc "Fetching new changes from %s" $remote]] + set cmds [list] + lappend cmds [list exec git fetch $remote] + if {[is_config_true gui.pruneduringfetch]} { + lappend cmds [list exec git remote prune $remote] + } + console::chain $w $cmds +} + +proc prune_from {remote} { + set w [console::new \ + [mc "remote prune %s" $remote] \ + [mc "Pruning tracking branches deleted from %s" $remote]] + console::exec $w [list git remote prune $remote] +} + +proc fetch_from_all {} { + set w [console::new \ + [mc "fetch all remotes"] \ + [mc "Fetching new changes from all remotes"]] + + set cmd [list git fetch --all] + if {[is_config_true gui.pruneduringfetch]} { + lappend cmd --prune + } + + console::exec $w $cmd +} + +proc prune_from_all {} { + global all_remotes + + set w [console::new \ + [mc "remote prune all remotes"] \ + [mc "Pruning tracking branches deleted from all remotes"]] + + set cmd [list git remote prune] + + foreach r $all_remotes { + lappend cmd $r + } + + console::exec $w $cmd +} + +proc push_to {remote} { + set w [console::new \ + [mc "push %s" $remote] \ + [mc "Pushing changes to %s" $remote]] + set cmd [list git push] + lappend cmd -v + lappend cmd $remote + console::exec $w $cmd +} + +proc start_push_anywhere_action {w} { + global push_urltype push_remote push_url push_thin push_tags + global push_force + global repo_config + + set is_mirror 0 + set r_url {} + switch -- $push_urltype { + remote { + set r_url $push_remote + catch {set is_mirror $repo_config(remote.$push_remote.mirror)} + } + url {set r_url $push_url} + } + if {$r_url eq {}} return + + set cmd [list git push] + lappend cmd -v + if {$push_thin} { + lappend cmd --thin + } + if {$push_force} { + lappend cmd --force + } + if {$push_tags} { + lappend cmd --tags + } + lappend cmd $r_url + if {$is_mirror} { + set cons [console::new \ + [mc "push %s" $r_url] \ + [mc "Mirroring to %s" $r_url]] + } else { + set cnt 0 + foreach i [$w.source.l curselection] { + set b [$w.source.l get $i] + lappend cmd "refs/heads/$b:refs/heads/$b" + incr cnt + } + if {$cnt == 0} { + return + } elseif {$cnt == 1} { + set unit branch + } else { + set unit branches + } + + set cons [console::new \ + [mc "push %s" $r_url] \ + [mc "Pushing %s %s to %s" $cnt $unit $r_url]] + } + console::exec $cons $cmd + destroy $w +} + +trace add variable push_remote write \ + [list radio_selector push_urltype remote] + +proc do_push_anywhere {} { + global all_remotes current_branch + global push_urltype push_remote push_url push_thin push_tags + global push_force use_ttk NS + + set w .push_setup + toplevel $w + catch {wm attributes $w -type dialog} + wm withdraw $w + wm geometry $w "+[winfo rootx .]+[winfo rooty .]" + pave_toplevel $w + + ${NS}::label $w.header -text [mc "Push Branches"] \ + -font font_uibold -anchor center + pack $w.header -side top -fill x + + ${NS}::frame $w.buttons + ${NS}::button $w.buttons.create -text [mc Push] \ + -default active \ + -command [list start_push_anywhere_action $w] + pack $w.buttons.create -side right + ${NS}::button $w.buttons.cancel -text [mc "Cancel"] \ + -default normal \ + -command [list destroy $w] + pack $w.buttons.cancel -side right -padx 5 + pack $w.buttons -side bottom -fill x -pady 10 -padx 10 + + ${NS}::labelframe $w.source -text [mc "Source Branches"] + slistbox $w.source.l \ + -height 10 \ + -width 70 \ + -selectmode extended + foreach h [load_all_heads] { + $w.source.l insert end $h + if {$h eq $current_branch} { + $w.source.l select set end + $w.source.l yview end + } + } + pack $w.source.l -side left -fill both -expand 1 + pack $w.source -fill both -expand 1 -pady 5 -padx 5 + + ${NS}::labelframe $w.dest -text [mc "Destination Repository"] + if {$all_remotes ne {}} { + ${NS}::radiobutton $w.dest.remote_r \ + -text [mc "Remote:"] \ + -value remote \ + -variable push_urltype + if {$use_ttk} { + ttk::combobox $w.dest.remote_m -state readonly \ + -exportselection false \ + -textvariable push_remote \ + -values $all_remotes + } else { + eval tk_optionMenu $w.dest.remote_m push_remote $all_remotes + } + grid $w.dest.remote_r $w.dest.remote_m -sticky w + if {[lsearch -sorted -exact $all_remotes origin] != -1} { + set push_remote origin + } else { + set push_remote [lindex $all_remotes 0] + } + set push_urltype remote + } else { + set push_urltype url + } + ${NS}::radiobutton $w.dest.url_r \ + -text [mc "Arbitrary Location:"] \ + -value url \ + -variable push_urltype + ${NS}::entry $w.dest.url_t \ + -width 50 \ + -textvariable push_url \ + -validate key \ + -validatecommand { + if {%d == 1 && [regexp {\s} %S]} {return 0} + if {%d == 1 && [string length %S] > 0} { + set push_urltype url + } + return 1 + } + grid $w.dest.url_r $w.dest.url_t -sticky we -padx {0 5} + grid columnconfigure $w.dest 1 -weight 1 + pack $w.dest -anchor nw -fill x -pady 5 -padx 5 + + ${NS}::labelframe $w.options -text [mc "Transfer Options"] + ${NS}::checkbutton $w.options.force \ + -text [mc "Force overwrite existing branch (may discard changes)"] \ + -variable push_force + grid $w.options.force -columnspan 2 -sticky w + ${NS}::checkbutton $w.options.thin \ + -text [mc "Use thin pack (for slow network connections)"] \ + -variable push_thin + grid $w.options.thin -columnspan 2 -sticky w + ${NS}::checkbutton $w.options.tags \ + -text [mc "Include tags"] \ + -variable push_tags + grid $w.options.tags -columnspan 2 -sticky w + grid columnconfigure $w.options 1 -weight 1 + pack $w.options -anchor nw -fill x -pady 5 -padx 5 + + set push_url {} + set push_force 0 + set push_thin 0 + set push_tags 0 + + bind $w <Visibility> "grab $w; focus $w.buttons.create" + bind $w <Key-Escape> "destroy $w" + bind $w <Key-Return> [list start_push_anywhere_action $w] + wm title $w [mc "%s (%s): Push" [appname] [reponame]] + wm deiconify $w + tkwait window $w +} diff --git a/git-gui/lib/win32.tcl b/git-gui/lib/win32.tcl new file mode 100644 index 0000000..db91ab8 --- /dev/null +++ b/git-gui/lib/win32.tcl @@ -0,0 +1,26 @@ +# git-gui Misc. native Windows 32 support +# Copyright (C) 2007 Shawn Pearce + +proc win32_read_lnk {lnk_path} { + return [exec cscript.exe \ + /E:jscript \ + /nologo \ + [file join $::oguilib win32_shortcut.js] \ + $lnk_path] +} + +proc win32_create_lnk {lnk_path lnk_exec lnk_dir} { + global oguilib + + set lnk_args [lrange $lnk_exec 1 end] + set lnk_exec [lindex $lnk_exec 0] + + eval [list exec wscript.exe \ + /E:jscript \ + /nologo \ + [file nativename [file join $oguilib win32_shortcut.js]] \ + $lnk_path \ + [file nativename [file join $oguilib git-gui.ico]] \ + $lnk_dir \ + $lnk_exec] $lnk_args +} diff --git a/git-gui/lib/win32_shortcut.js b/git-gui/lib/win32_shortcut.js new file mode 100644 index 0000000..117923f --- /dev/null +++ b/git-gui/lib/win32_shortcut.js @@ -0,0 +1,34 @@ +// git-gui Windows shortcut support +// Copyright (C) 2007 Shawn Pearce + +var WshShell = WScript.CreateObject("WScript.Shell"); +var argv = WScript.Arguments; +var argi = 0; +var lnk_path = argv.item(argi++); +var ico_path = argi < argv.length ? argv.item(argi++) : undefined; +var dir_path = argi < argv.length ? argv.item(argi++) : undefined; +var lnk_exec = argi < argv.length ? argv.item(argi++) : undefined; +var lnk_args = ''; +while (argi < argv.length) { + var s = argv.item(argi++); + if (lnk_args != '') + lnk_args += ' '; + if (s.indexOf(' ') >= 0) { + lnk_args += '"'; + lnk_args += s; + lnk_args += '"'; + } else { + lnk_args += s; + } +} + +var lnk = WshShell.CreateShortcut(lnk_path); +if (argv.length == 1) { + WScript.echo(lnk.TargetPath); +} else { + lnk.TargetPath = lnk_exec; + lnk.Arguments = lnk_args; + lnk.IconLocation = ico_path + ", 0"; + lnk.WorkingDirectory = dir_path; + lnk.Save(); +} diff --git a/git-gui/macosx/AppMain.tcl b/git-gui/macosx/AppMain.tcl new file mode 100644 index 0000000..b6c6dc3 --- /dev/null +++ b/git-gui/macosx/AppMain.tcl @@ -0,0 +1,29 @@ +set gitexecdir {@@gitexecdir@@} +if { [info exists ::env(GIT_GUI_LIB_DIR) ] } { + set gitguilib $::env(GIT_GUI_LIB_DIR) +} else { + set gitguilib {@@GITGUI_LIBDIR@@} +} + +set env(PATH) "$gitexecdir:$env(PATH)" + +if {[string first -psn [lindex $argv 0]] == 0} { + lset argv 0 [file join $gitexecdir git-gui] +} + +if {[file tail [lindex $argv 0]] eq {gitk}} { + set argv0 [lindex $argv 0] + set AppMain_source $argv0 +} else { + set argv0 [file join $gitexecdir [file tail [lindex $argv 0]]] + set AppMain_source [file join $gitguilib git-gui.tcl] + if {[info exists env(PWD)]} { + cd $env(PWD) + } elseif {[pwd] eq {/}} { + cd $env(HOME) + } +} + +unset gitexecdir gitguilib +set argv [lrange $argv 1 end] +source $AppMain_source diff --git a/git-gui/macosx/Info.plist b/git-gui/macosx/Info.plist new file mode 100644 index 0000000..1ade121 --- /dev/null +++ b/git-gui/macosx/Info.plist @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>English</string> + <key>CFBundleExecutable</key> + <string>@@GITGUI_TKEXECUTABLE@@</string> + <key>CFBundleGetInfoString</key> + <string>Git Gui @@GITGUI_VERSION@@ © 2006-2007 Shawn Pearce, et. al.</string> + <key>CFBundleIconFile</key> + <string>git-gui.icns</string> + <key>CFBundleIdentifier</key> + <string>cz.or.repo.git-gui</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>Git Gui</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>@@GITGUI_VERSION@@</string> + <key>CFBundleSignature</key> + <string>GITg</string> + <key>CFBundleVersion</key> + <string>@@GITGUI_VERSION@@</string> + <key>NSHighResolutionCapable</key> + <true/> +</dict> +</plist> diff --git a/git-gui/macosx/git-gui.icns b/git-gui/macosx/git-gui.icns Binary files differnew file mode 100644 index 0000000..77d88a7 --- /dev/null +++ b/git-gui/macosx/git-gui.icns diff --git a/git-gui/po/.gitignore b/git-gui/po/.gitignore new file mode 100644 index 0000000..a89cf44 --- /dev/null +++ b/git-gui/po/.gitignore @@ -0,0 +1,2 @@ +*.msg +*~ diff --git a/git-gui/po/README b/git-gui/po/README new file mode 100644 index 0000000..2514bc2 --- /dev/null +++ b/git-gui/po/README @@ -0,0 +1,251 @@ +Localizing git-gui for your language +==================================== + +This short note is to help you, who reads and writes English and your +own language, help us getting git-gui localized for more languages. It +does not try to be a comprehensive manual of GNU gettext, which is the +i18n framework we use, but tries to help you get started by covering the +basics and how it is used in this project. + +1. Getting started. + +You would first need to have a working "git". Your distribution may +have it as "git-core" package (do not get "GNU Interactive Tools" -- +that is a different "git"). You would also need GNU gettext toolchain +to test the resulting translation out. Although you can work on message +translation files with a regular text editor, it is a good idea to have +specialized so-called "po file editors" (e.g. emacs po-mode, KBabel, +poedit, GTranslator --- any of them would work well). Please install +them. + +You would then need to clone the git-gui project repository and create +a feature branch to begin working: + + $ git clone git://repo.or.cz/git-gui.git + $ cd git-gui.git + $ git checkout -b my-translation + +The "git checkout" command creates a new branch to keep your work +isolated and to make it simple to post your patch series when +completed. You will be working on this branch. + + +2. Starting a new language. + +In the git-gui directory is a po/ subdirectory. It has a handful of +files whose names end with ".po". Is there a file that has messages +in your language? + +If you do not know what your language should be named, you need to find +it. This currently follows ISO 639-1 two letter codes: + + http://www.loc.gov/standards/iso639-2/php/code_list.php + +For example, if you are preparing a translation for Afrikaans, the +language code is "af". If there already is a translation for your +language, you do not have to perform any step in this section, but keep +reading, because we are covering the basics. + +If you did not find your language, you would need to start one yourself. +Copy po/git-gui.pot file to po/af.po (replace "af" with the code for +your language). Edit the first several lines to match existing *.po +files to make it clear this is a translation table for git-gui project, +and you are the primary translator. The result of your editing would +look something like this: + + # Translation of git-gui to Afrikaans + # Copyright (C) 2007 Shawn Pearce + # This file is distributed under the same license as the git-gui package. + # YOUR NAME <YOUR@E-MAIL.ADDRESS>, 2007. + # + #, fuzzy + msgid "" + msgstr "" + "Project-Id-Version: git-gui\n" + "Report-Msgid-Bugs-To: \n" + "POT-Creation-Date: 2007-07-24 22:19+0300\n" + "PO-Revision-Date: 2007-07-25 18:00+0900\n" + "Last-Translator: YOUR NAME <YOUR@E-MAIL.ADDRESS>\n" + "Language-Team: Afrikaans\n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=UTF-8\n" + "Content-Transfer-Encoding: 8bit\n" + +You will find many pairs of a "msgid" line followed by a "msgstr" line. +These pairs define how messages in git-gui application are translated to +your language. Your primarily job is to fill in the empty double quote +pairs on msgstr lines with the translation of the strings on their +matching msgid lines. A few tips: + + - Control characters, such as newlines, are written in backslash + sequence similar to string literals in the C programming language. + When the string given on a msgid line has such a backslash sequence, + you would typically want to have corresponding ones in the string on + your msgstr line. + + - Some messages contain an optional context indicator at the end, + for example "@@noun" or "@@verb". This indicator allows the + software to select the correct translation depending upon the use. + The indicator is not actually part of the message and will not + be shown to the end-user. + + If your language does not require a different translation you + will still need to translate both messages. + + - Often the messages being translated are format strings given to + "printf()"-like functions. Make sure "%s", "%d", and "%%" in your + translated messages match the original. + + When you have to change the order of words, you can add "<number>$" + between '%' and the conversion ('s', 'd', etc.) to say "<number>-th + parameter to the format string is used at this point". For example, + if the original message is like this: + + "Length is %d, Weight is %d" + + and if for whatever reason your translation needs to say weight first + and then length, you can say something like: + + "WEIGHT IS %2$d, LENGTH IS %1$d" + + A format specification with a '*' (asterisk) refers to *two* arguments + instead of one, hence the succeeding argument number is two higher + instead of one. So, a message like this + + "%s ... %*i of %*i %s (%3i%%)" + + is equivalent to + + "%1$s ... %2$*i of %4$*i %6$s (%7$3i%%)" + + - A long message can be split across multiple lines by ending the + string with a double quote, and starting another string on the next + line with another double quote. They will be concatenated in the + result. For example: + + #: lib/remote_branch_delete.tcl:189 + #, tcl-format + msgid "" + "One or more of the merge tests failed because you have not fetched the " + "necessary commits. Try fetching from %s first." + msgstr "" + "HERE YOU WILL WRITE YOUR TRANSLATION OF THE ABOVE LONG " + "MESSAGE IN YOUR LANGUAGE." + +You can test your translation by running "make install", which would +create po/af.msg file and installs the result, and then running the +resulting git-gui under your locale: + + $ make install + $ LANG=af git-gui + +There is a trick to test your translation without first installing: + + $ make + $ LANG=af ./git-gui.sh + +When you are satisfied with your translation, commit your changes then submit +your patch series to the maintainer and the Git mailing list: + + $ edit po/af.po + ... be sure to update Last-Translator: and + ... PO-Revision-Date: lines. + $ git add po/af.po + $ git commit -s -m 'git-gui: added Afrikaans translation.' + $ git send-email --to 'git@vger.kernel.org' \ + --cc 'Pat Thoyts <patthoyts@users.sourceforge.net>' \ + --subject 'git-gui: Afrikaans translation' \ + master.. + + +3. Updating your translation. + +There may already be a translation for your language, and you may want +to contribute an update. This may be because you would want to improve +the translation of existing messages, or because the git-gui software +itself was updated and there are new messages that need translation. + +In any case, make sure you are up to date before starting your work: + + $ git checkout master + $ git pull + +In the former case, you will edit po/af.po (again, replace "af" with +your language code), and after testing and updating the Last-Translator: +and PO-Revision-Date: lines, "add/commit/push" as in the previous +section. + +By comparing "POT-Creation-Date:" line in po/git-gui.pot file and +po/af.po file, you can tell if there are new messages that need to be +translated. You would need the GNU gettext package to perform this +step. + + $ msgmerge -U po/af.po po/git-gui.pot + +This updates po/af.po (again, replace "af" with your language +code) so that it contains msgid lines (i.e. the original) that +your translation did not have before. There are a few things to +watch out for: + + - The original text in English of an older message you already + translated might have been changed. You will notice a comment line + that begins with "#, fuzzy" in front of such a message. msgmerge + tool made its best effort to match your old translation with the + message from the updated software, but you may find cases that it + matched your old translated message to a new msgid and the pairing + does not make any sense -- you would need to fix them, and then + remove the "#, fuzzy" line from the message (your fixed translation + of the message will not be used before you remove the marker). + + - New messages added to the software will have msgstr lines with empty + strings. You would need to translate them. + +The po/git-gui.pot file is updated by the internationalization +coordinator from time to time. You _could_ update it yourself, but +translators are discouraged from doing so because we would want all +language teams to be working off of the same version of git-gui.pot. + +**************************************************************** + +This section is a note to the internationalization coordinator, and +translators do not have to worry about it too much. + +The message template file po/git-gui.pot needs to be kept up to date +relative to the software the translations apply to, and it is the +responsibility of the internationalization coordinator. + +When updating po/git-gui.pot file, however, _never_ run "msgmerge -U +po/xx.po" for individual language translations, unless you are absolutely +sure that there is no outstanding work on translation for language xx. +Doing so will create unnecessary merge conflicts and force needless +re-translation on translators. The translator however may not have access +to the msgmerge tool, in which case the coordinator may run it for the +translator as a service. + +But mistakes do happen. Suppose a translation was based on an older +version X, the POT file was updated at version Y and then msgmerge was run +at version Z for the language, and the translator sent in a patch based on +version X: + + ? translated + / + ---X---Y---Z (master) + +The coordinator could recover from such a mistake by first applying the +patch to X, replace the translated file in Z, and then running msgmerge +again based on the updated POT file and commit the result. The sequence +would look like this: + + $ git checkout X + $ git am -s xx.patch + $ git checkout master + $ git checkout HEAD@{1} po/xx.po + $ msgmerge -U po/xx.po po/git-gui.pot + $ git commit -c HEAD@{1} po/xx.po + +State in the message that the translated messages are based on a slightly +older version, and msgmerge was run to incorporate changes to message +templates from the updated POT file. The result needs to be further +translated, but at least the messages that were updated by the patch that +were not changed by the POT update will survive the process and do not +need to be re-translated. diff --git a/git-gui/po/bg.po b/git-gui/po/bg.po new file mode 100644 index 0000000..5af78f1 --- /dev/null +++ b/git-gui/po/bg.po @@ -0,0 +1,2807 @@ +# Bulgarian translation of git-gui po-file. +# Copyright (C) 2012, 2013, 2014, 2015, 2016 Alexander Shopov <ash@kambanaria.org>. +# This file is distributed under the same license as the git package. +# Alexander Shopov <ash@kambanaria.org>, 2012, 2013, 2014, 2015, 2016. +# +# +msgid "" +msgstr "" +"Project-Id-Version: git-gui master\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2016-10-13 15:16+0300\n" +"PO-Revision-Date: 2016-10-13 15:16+0300\n" +"Last-Translator: Alexander Shopov <ash@kambanaria.org>\n" +"Language-Team: Bulgarian <dict@fsa-bg.org>\n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: git-gui.sh:865 +#, tcl-format +msgid "Invalid font specified in %s:" +msgstr "Указан е неправилен шрифт в „%s“:" + +#: git-gui.sh:919 +msgid "Main Font" +msgstr "Основен шрифт" + +#: git-gui.sh:920 +msgid "Diff/Console Font" +msgstr "Шрифт за разликите/конзолата" + +#: git-gui.sh:935 git-gui.sh:949 git-gui.sh:962 git-gui.sh:1052 git-gui.sh:1071 +#: git-gui.sh:3147 +msgid "git-gui: fatal error" +msgstr "git-gui: фатална грешка" + +#: git-gui.sh:936 +msgid "Cannot find git in PATH." +msgstr "Командата git липсва в пътя (PATH)." + +#: git-gui.sh:963 +msgid "Cannot parse Git version string:" +msgstr "Низът с версията на Git не може да бъде интерпретиран:" + +#: git-gui.sh:988 +#, tcl-format +msgid "" +"Git version cannot be determined.\n" +"\n" +"%s claims it is version '%s'.\n" +"\n" +"%s requires at least Git 1.5.0 or later.\n" +"\n" +"Assume '%s' is version 1.5.0?\n" +msgstr "" +"Версията на Git не може да бъде определена.\n" +"\n" +"Версията на „%s“ изглежда, че е „%s“.\n" +"\n" +"„%s“ изисква Git, версия поне 1.5.0.\n" +"\n" +"Да се приеме ли, че „%s“ е версия „1.5.0“?\n" + +#: git-gui.sh:1285 +msgid "Git directory not found:" +msgstr "Директорията на Git не е открита:" + +#: git-gui.sh:1319 +msgid "Cannot move to top of working directory:" +msgstr "Не може да се премине към родителската директория." + +#: git-gui.sh:1327 +msgid "Cannot use bare repository:" +msgstr "Голо хранилище не може да се използва:" + +#: git-gui.sh:1335 +msgid "No working directory" +msgstr "Работната директория липсва" + +#: git-gui.sh:1507 lib/checkout_op.tcl:306 +msgid "Refreshing file status..." +msgstr "Обновяване на състоянието на файла…" + +#: git-gui.sh:1567 +msgid "Scanning for modified files ..." +msgstr "Проверка за променени файлове…" + +#: git-gui.sh:1645 +msgid "Calling prepare-commit-msg hook..." +msgstr "Куката „prepare-commit-msg“ се изпълнява в момента…" + +#: git-gui.sh:1662 +msgid "Commit declined by prepare-commit-msg hook." +msgstr "Подаването е отхвърлено от куката „prepare-commit-msg“." + +#: git-gui.sh:1820 lib/browser.tcl:252 +msgid "Ready." +msgstr "Готово." + +#: git-gui.sh:1984 +#, tcl-format +msgid "" +"Display limit (gui.maxfilesdisplayed = %s) reached, not showing all %s files." +msgstr "" +"Достигнат е максималният размер на списъка за извеждане(gui." +"maxfilesdisplayed = %s), съответно не са показани всички %s файла." + +#: git-gui.sh:2107 +msgid "Unmodified" +msgstr "Непроменен" + +#: git-gui.sh:2109 +msgid "Modified, not staged" +msgstr "Променен, но не е в индекса" + +#: git-gui.sh:2110 git-gui.sh:2122 +msgid "Staged for commit" +msgstr "В индекса за подаване" + +#: git-gui.sh:2111 git-gui.sh:2123 +msgid "Portions staged for commit" +msgstr "Части са в индекса за подаване" + +#: git-gui.sh:2112 git-gui.sh:2124 +msgid "Staged for commit, missing" +msgstr "В индекса за подаване, но липсва" + +#: git-gui.sh:2114 +msgid "File type changed, not staged" +msgstr "Видът на файла е сменен, но не е в индекса" + +#: git-gui.sh:2115 git-gui.sh:2116 +msgid "File type changed, old type staged for commit" +msgstr "Видът на файла е сменен, но новият вид не е в индекса" + +#: git-gui.sh:2117 +msgid "File type changed, staged" +msgstr "Видът на файла е сменен и е в индекса" + +#: git-gui.sh:2118 +msgid "File type change staged, modification not staged" +msgstr "Видът на файла е сменен в индекса, но не и съдържанието" + +#: git-gui.sh:2119 +msgid "File type change staged, file missing" +msgstr "Видът на файла е сменен в индекса, но файлът липсва" + +#: git-gui.sh:2121 +msgid "Untracked, not staged" +msgstr "Неследен" + +#: git-gui.sh:2126 +msgid "Missing" +msgstr "Липсващ" + +#: git-gui.sh:2127 +msgid "Staged for removal" +msgstr "В индекса за изтриване" + +#: git-gui.sh:2128 +msgid "Staged for removal, still present" +msgstr "В индекса за изтриване, но още го има" + +#: git-gui.sh:2130 git-gui.sh:2131 git-gui.sh:2132 git-gui.sh:2133 +#: git-gui.sh:2134 git-gui.sh:2135 +msgid "Requires merge resolution" +msgstr "Изисква коригиране при сливане" + +#: git-gui.sh:2170 +msgid "Starting gitk... please wait..." +msgstr "Стартиране на „gitk“…, изчакайте…" + +#: git-gui.sh:2182 +msgid "Couldn't find gitk in PATH" +msgstr "Командата „gitk“ липсва в пътищата, определени от променливата PATH." + +#: git-gui.sh:2241 +msgid "Couldn't find git gui in PATH" +msgstr "" +"Командата „git gui“ липсва в пътищата, определени от променливата PATH." + +#: git-gui.sh:2676 lib/choose_repository.tcl:41 +msgid "Repository" +msgstr "Хранилище" + +#: git-gui.sh:2677 +msgid "Edit" +msgstr "Редактиране" + +#: git-gui.sh:2679 lib/choose_rev.tcl:567 +msgid "Branch" +msgstr "Клон" + +#: git-gui.sh:2682 lib/choose_rev.tcl:554 +msgid "Commit@@noun" +msgstr "Подаване" + +#: git-gui.sh:2685 lib/merge.tcl:127 lib/merge.tcl:174 +msgid "Merge" +msgstr "Сливане" + +#: git-gui.sh:2686 lib/choose_rev.tcl:563 +msgid "Remote" +msgstr "Отдалечено хранилище" + +#: git-gui.sh:2689 +msgid "Tools" +msgstr "Команди" + +#: git-gui.sh:2698 +msgid "Explore Working Copy" +msgstr "Разглеждане на работното копие" + +#: git-gui.sh:2704 +msgid "Git Bash" +msgstr "Bash за Git" + +#: git-gui.sh:2714 +msgid "Browse Current Branch's Files" +msgstr "Разглеждане на файловете в текущия клон" + +#: git-gui.sh:2718 +msgid "Browse Branch Files..." +msgstr "Разглеждане на текущия клон…" + +#: git-gui.sh:2723 +msgid "Visualize Current Branch's History" +msgstr "Визуализация на историята на текущия клон" + +#: git-gui.sh:2727 +msgid "Visualize All Branch History" +msgstr "Визуализация на историята на всички клонове" + +#: git-gui.sh:2734 +#, tcl-format +msgid "Browse %s's Files" +msgstr "Разглеждане на файловете в „%s“" + +#: git-gui.sh:2736 +#, tcl-format +msgid "Visualize %s's History" +msgstr "Визуализация на историята на „%s“" + +#: git-gui.sh:2741 lib/database.tcl:40 +msgid "Database Statistics" +msgstr "Статистика на базата от данни" + +#: git-gui.sh:2744 lib/database.tcl:33 +msgid "Compress Database" +msgstr "Компресиране на базата от данни" + +#: git-gui.sh:2747 +msgid "Verify Database" +msgstr "Проверка на базата от данни" + +#: git-gui.sh:2754 git-gui.sh:2758 git-gui.sh:2762 +msgid "Create Desktop Icon" +msgstr "Добавяне на икона на работния плот" + +#: git-gui.sh:2770 lib/choose_repository.tcl:193 lib/choose_repository.tcl:201 +msgid "Quit" +msgstr "Спиране на програмата" + +#: git-gui.sh:2778 +msgid "Undo" +msgstr "Отмяна" + +#: git-gui.sh:2781 +msgid "Redo" +msgstr "Повторение" + +#: git-gui.sh:2785 git-gui.sh:3399 +msgid "Cut" +msgstr "Отрязване" + +#: git-gui.sh:2788 git-gui.sh:3402 git-gui.sh:3476 git-gui.sh:3562 +#: lib/console.tcl:69 +msgid "Copy" +msgstr "Копиране" + +#: git-gui.sh:2791 git-gui.sh:3405 +msgid "Paste" +msgstr "Поставяне" + +#: git-gui.sh:2794 git-gui.sh:3408 lib/branch_delete.tcl:28 +#: lib/remote_branch_delete.tcl:39 +msgid "Delete" +msgstr "Изтриване" + +#: git-gui.sh:2798 git-gui.sh:3412 git-gui.sh:3566 lib/console.tcl:71 +msgid "Select All" +msgstr "Избиране на всичко" + +#: git-gui.sh:2807 +msgid "Create..." +msgstr "Създаване…" + +#: git-gui.sh:2813 +msgid "Checkout..." +msgstr "Изтегляне…" + +#: git-gui.sh:2819 +msgid "Rename..." +msgstr "Преименуване…" + +#: git-gui.sh:2824 +msgid "Delete..." +msgstr "Изтриване…" + +#: git-gui.sh:2829 +msgid "Reset..." +msgstr "Отмяна на промените…" + +#: git-gui.sh:2839 +msgid "Done" +msgstr "Готово" + +#: git-gui.sh:2841 +msgid "Commit@@verb" +msgstr "Подаване" + +#: git-gui.sh:2850 git-gui.sh:3335 +msgid "New Commit" +msgstr "Ново подаване" + +#: git-gui.sh:2858 git-gui.sh:3342 +msgid "Amend Last Commit" +msgstr "Поправяне на последното подаване" + +#: git-gui.sh:2868 git-gui.sh:3296 lib/remote_branch_delete.tcl:101 +msgid "Rescan" +msgstr "Обновяване" + +#: git-gui.sh:2874 +msgid "Stage To Commit" +msgstr "Към индекса за подаване" + +#: git-gui.sh:2880 +msgid "Stage Changed Files To Commit" +msgstr "Всички променени файлове към индекса за подаване" + +#: git-gui.sh:2886 +msgid "Unstage From Commit" +msgstr "Изваждане от индекса за подаване" + +#: git-gui.sh:2892 lib/index.tcl:442 +msgid "Revert Changes" +msgstr "Връщане на оригинала" + +#: git-gui.sh:2900 git-gui.sh:3613 git-gui.sh:3644 +msgid "Show Less Context" +msgstr "По-малко контекст" + +#: git-gui.sh:2904 git-gui.sh:3617 git-gui.sh:3648 +msgid "Show More Context" +msgstr "Повече контекст" + +#: git-gui.sh:2911 git-gui.sh:3309 git-gui.sh:3423 +msgid "Sign Off" +msgstr "Подписване" + +#: git-gui.sh:2927 +msgid "Local Merge..." +msgstr "Локално сливане…" + +#: git-gui.sh:2932 +msgid "Abort Merge..." +msgstr "Преустановяване на сливане…" + +#: git-gui.sh:2944 git-gui.sh:2972 +msgid "Add..." +msgstr "Добавяне…" + +#: git-gui.sh:2948 +msgid "Push..." +msgstr "Изтласкване…" + +#: git-gui.sh:2952 +msgid "Delete Branch..." +msgstr "Изтриване на клон…" + +#: git-gui.sh:2962 git-gui.sh:3595 +msgid "Options..." +msgstr "Опции…" + +#: git-gui.sh:2973 +msgid "Remove..." +msgstr "Премахване…" + +#: git-gui.sh:2982 lib/choose_repository.tcl:55 +msgid "Help" +msgstr "Помощ" + +#: git-gui.sh:2986 git-gui.sh:2990 lib/about.tcl:14 +#: lib/choose_repository.tcl:49 lib/choose_repository.tcl:58 +#, tcl-format +msgid "About %s" +msgstr "Относно %s" + +#: git-gui.sh:3014 +msgid "Online Documentation" +msgstr "Документация в Интернет" + +#: git-gui.sh:3017 lib/choose_repository.tcl:52 lib/choose_repository.tcl:61 +msgid "Show SSH Key" +msgstr "Показване на ключа за SSH" + +#: git-gui.sh:3032 git-gui.sh:3164 +msgid "usage:" +msgstr "употреба:" + +#: git-gui.sh:3036 git-gui.sh:3168 +msgid "Usage" +msgstr "Употреба" + +#: git-gui.sh:3117 lib/blame.tcl:573 +msgid "Error" +msgstr "Грешка" + +#: git-gui.sh:3148 +#, tcl-format +msgid "fatal: cannot stat path %s: No such file or directory" +msgstr "" +"ФАТАЛНА ГРЕШКА: пътят %s не може да бъде открит: такъв файл или директория " +"няма" + +#: git-gui.sh:3181 +msgid "Current Branch:" +msgstr "Текущ клон:" + +#: git-gui.sh:3206 +msgid "Unstaged Changes" +msgstr "Промени извън индекса" + +#: git-gui.sh:3228 +msgid "Staged Changes (Will Commit)" +msgstr "Промени в индекса (за подаване)" + +#: git-gui.sh:3302 +msgid "Stage Changed" +msgstr "Индексът е променен" + +#: git-gui.sh:3321 lib/transport.tcl:137 +msgid "Push" +msgstr "Изтласкване" + +#: git-gui.sh:3356 +msgid "Initial Commit Message:" +msgstr "Първоначално съобщение при подаване:" + +#: git-gui.sh:3357 +msgid "Amended Commit Message:" +msgstr "Поправено съобщение при подаване:" + +#: git-gui.sh:3358 +msgid "Amended Initial Commit Message:" +msgstr "Поправено първоначално съобщение при подаване:" + +#: git-gui.sh:3359 +msgid "Amended Merge Commit Message:" +msgstr "Поправено съобщение при подаване със сливане:" + +#: git-gui.sh:3360 +msgid "Merge Commit Message:" +msgstr "Съобщение при подаване със сливане:" + +#: git-gui.sh:3361 +msgid "Commit Message:" +msgstr "Съобщение при подаване:" + +#: git-gui.sh:3415 git-gui.sh:3570 lib/console.tcl:73 +msgid "Copy All" +msgstr "Копиране на всичко" + +#: git-gui.sh:3439 lib/blame.tcl:105 +msgid "File:" +msgstr "Файл:" + +#: git-gui.sh:3558 +msgid "Refresh" +msgstr "Обновяване" + +#: git-gui.sh:3579 +msgid "Decrease Font Size" +msgstr "По-едър шрифт" + +#: git-gui.sh:3583 +msgid "Increase Font Size" +msgstr "По-дребен шрифт" + +#: git-gui.sh:3591 lib/blame.tcl:294 +msgid "Encoding" +msgstr "Кодиране" + +#: git-gui.sh:3602 +msgid "Apply/Reverse Hunk" +msgstr "Прилагане/връщане на парче" + +#: git-gui.sh:3607 +msgid "Apply/Reverse Line" +msgstr "Прилагане/връщане на ред" + +#: git-gui.sh:3626 +msgid "Run Merge Tool" +msgstr "Изпълнение на програмата за сливане" + +#: git-gui.sh:3631 +msgid "Use Remote Version" +msgstr "Версия от отдалеченото хранилище" + +#: git-gui.sh:3635 +msgid "Use Local Version" +msgstr "Локална версия" + +#: git-gui.sh:3639 +msgid "Revert To Base" +msgstr "Връщане към родителската версия" + +#: git-gui.sh:3657 +msgid "Visualize These Changes In The Submodule" +msgstr "Визуализиране на промените в подмодула" + +#: git-gui.sh:3661 +msgid "Visualize Current Branch History In The Submodule" +msgstr "Визуализация на историята на текущия клон в историята за подмодула" + +#: git-gui.sh:3665 +msgid "Visualize All Branch History In The Submodule" +msgstr "Визуализация на историята на всички клони в историята за подмодула" + +#: git-gui.sh:3670 +msgid "Start git gui In The Submodule" +msgstr "Стартиране на „git gui“ за подмодула" + +#: git-gui.sh:3705 +msgid "Unstage Hunk From Commit" +msgstr "Изваждане на парчето от подаването" + +#: git-gui.sh:3707 +msgid "Unstage Lines From Commit" +msgstr "Изваждане на редовете от подаването" + +#: git-gui.sh:3709 +msgid "Unstage Line From Commit" +msgstr "Изваждане на реда от подаването" + +#: git-gui.sh:3712 +msgid "Stage Hunk For Commit" +msgstr "Добавяне на парчето за подаване" + +#: git-gui.sh:3714 +msgid "Stage Lines For Commit" +msgstr "Добавяне на редовете за подаване" + +#: git-gui.sh:3716 +msgid "Stage Line For Commit" +msgstr "Добавяне на реда за подаване" + +#: git-gui.sh:3741 +msgid "Initializing..." +msgstr "Инициализиране…" + +#: git-gui.sh:3886 +#, tcl-format +msgid "" +"Possible environment issues exist.\n" +"\n" +"The following environment variables are probably\n" +"going to be ignored by any Git subprocess run\n" +"by %s:\n" +"\n" +msgstr "" +"Възможно е да има проблем със средата.\n" +"\n" +"Най-вероятно следните променливи няма да бъдат\n" +"взети под внимание от подпроцесите на Git\n" +"от %s:\n" +"\n" + +#: git-gui.sh:3915 +msgid "" +"\n" +"This is due to a known issue with the\n" +"Tcl binary distributed by Cygwin." +msgstr "" +"\n" +"Това е познат проблем и се дължи на\n" +"версията на Tcl включена в Cygwin." + +#: git-gui.sh:3920 +#, tcl-format +msgid "" +"\n" +"\n" +"A good replacement for %s\n" +"is placing values for the user.name and\n" +"user.email settings into your personal\n" +"~/.gitconfig file.\n" +msgstr "" +"\n" +"\n" +"Добър заместител на „%s“\n" +"е да поставите настройките „user.name“ и\n" +"„user.email“ в личния си файл „~/.gitconfig“.\n" + +#: lib/about.tcl:26 +msgid "git-gui - a graphical user interface for Git." +msgstr "git-gui — графичен интерфейс за Git." + +#: lib/blame.tcl:73 +#, tcl-format +msgid "%s (%s): File Viewer" +msgstr "%s (%s): Преглед на файлове" + +#: lib/blame.tcl:79 +msgid "Commit:" +msgstr "Подаване:" + +#: lib/blame.tcl:280 +msgid "Copy Commit" +msgstr "Копиране на подаване" + +#: lib/blame.tcl:284 +msgid "Find Text..." +msgstr "Търсене на текст…" + +#: lib/blame.tcl:288 +msgid "Goto Line..." +msgstr "Към ред…" + +#: lib/blame.tcl:297 +msgid "Do Full Copy Detection" +msgstr "Пълно търсене на копиране" + +#: lib/blame.tcl:301 +msgid "Show History Context" +msgstr "Показване на контекста от историята" + +#: lib/blame.tcl:304 +msgid "Blame Parent Commit" +msgstr "Анотиране на родителското подаване" + +#: lib/blame.tcl:466 +#, tcl-format +msgid "Reading %s..." +msgstr "Чете се „%s“…" + +#: lib/blame.tcl:594 +msgid "Loading copy/move tracking annotations..." +msgstr "Зареждане на анотациите за проследяване на копирането/преместването…" + +#: lib/blame.tcl:614 +msgid "lines annotated" +msgstr "реда анотирани" + +#: lib/blame.tcl:806 +msgid "Loading original location annotations..." +msgstr "Зареждане на анотациите за първоначалното местоположение…" + +#: lib/blame.tcl:809 +msgid "Annotation complete." +msgstr "Анотирането завърши." + +#: lib/blame.tcl:839 +msgid "Busy" +msgstr "Операцията не е завършила" + +#: lib/blame.tcl:840 +msgid "Annotation process is already running." +msgstr "В момента тече процес на анотиране." + +#: lib/blame.tcl:879 +msgid "Running thorough copy detection..." +msgstr "Изпълнява се цялостен процес на откриване на копиране…" + +#: lib/blame.tcl:947 +msgid "Loading annotation..." +msgstr "Зареждане на анотации…" + +#: lib/blame.tcl:1000 +msgid "Author:" +msgstr "Автор:" + +#: lib/blame.tcl:1004 +msgid "Committer:" +msgstr "Подал:" + +#: lib/blame.tcl:1009 +msgid "Original File:" +msgstr "Първоначален файл:" + +#: lib/blame.tcl:1057 +msgid "Cannot find HEAD commit:" +msgstr "Подаването за връх „HEAD“ не може да се открие:" + +#: lib/blame.tcl:1112 +msgid "Cannot find parent commit:" +msgstr "Родителското подаване не може да бъде открито" + +#: lib/blame.tcl:1127 +msgid "Unable to display parent" +msgstr "Родителят не може да бъде показан" + +#: lib/blame.tcl:1128 lib/diff.tcl:358 +msgid "Error loading diff:" +msgstr "Грешка при зареждане на разлика:" + +#: lib/blame.tcl:1269 +msgid "Originally By:" +msgstr "Първоначално от:" + +#: lib/blame.tcl:1275 +msgid "In File:" +msgstr "Във файл:" + +#: lib/blame.tcl:1280 +msgid "Copied Or Moved Here By:" +msgstr "Копирано или преместено тук от:" + +#: lib/branch_checkout.tcl:16 +#, tcl-format +msgid "%s (%s): Checkout Branch" +msgstr "%s (%s): Клон за изтегляне" + +#: lib/branch_checkout.tcl:21 +msgid "Checkout Branch" +msgstr "Клон за изтегляне" + +#: lib/branch_checkout.tcl:26 +msgid "Checkout" +msgstr "Изтегляне" + +#: lib/branch_checkout.tcl:30 lib/branch_create.tcl:37 lib/branch_delete.tcl:34 +#: lib/branch_rename.tcl:32 lib/browser.tcl:292 lib/checkout_op.tcl:579 +#: lib/choose_font.tcl:45 lib/merge.tcl:178 lib/option.tcl:127 +#: lib/remote_add.tcl:34 lib/remote_branch_delete.tcl:43 lib/tools_dlg.tcl:41 +#: lib/tools_dlg.tcl:202 lib/tools_dlg.tcl:345 lib/transport.tcl:141 +msgid "Cancel" +msgstr "Отказване" + +#: lib/branch_checkout.tcl:35 lib/browser.tcl:297 lib/tools_dlg.tcl:321 +msgid "Revision" +msgstr "Версия" + +#: lib/branch_checkout.tcl:39 lib/branch_create.tcl:69 lib/option.tcl:310 +msgid "Options" +msgstr "Опции" + +#: lib/branch_checkout.tcl:42 lib/branch_create.tcl:92 +msgid "Fetch Tracking Branch" +msgstr "Изтегляне на промените от следения клон" + +#: lib/branch_checkout.tcl:47 +msgid "Detach From Local Branch" +msgstr "Изтриване от локалния клон" + +#: lib/branch_create.tcl:23 +#, tcl-format +msgid "%s (%s): Create Branch" +msgstr "%s (%s): Създаване на клон" + +#: lib/branch_create.tcl:28 +msgid "Create New Branch" +msgstr "Създаване на нов клон" + +#: lib/branch_create.tcl:33 lib/choose_repository.tcl:407 +msgid "Create" +msgstr "Създаване" + +#: lib/branch_create.tcl:42 +msgid "Branch Name" +msgstr "Име на клона" + +#: lib/branch_create.tcl:44 lib/remote_add.tcl:41 lib/tools_dlg.tcl:51 +msgid "Name:" +msgstr "Име:" + +#: lib/branch_create.tcl:57 +msgid "Match Tracking Branch Name" +msgstr "Съвпадане по името на следения клон" + +#: lib/branch_create.tcl:66 +msgid "Starting Revision" +msgstr "Начална версия" + +#: lib/branch_create.tcl:72 +msgid "Update Existing Branch:" +msgstr "Обновяване на съществуващ клон:" + +#: lib/branch_create.tcl:75 +msgid "No" +msgstr "Не" + +#: lib/branch_create.tcl:80 +msgid "Fast Forward Only" +msgstr "Само тривиално превъртащо сливане" + +#: lib/branch_create.tcl:85 lib/checkout_op.tcl:571 +msgid "Reset" +msgstr "Отначало" + +#: lib/branch_create.tcl:97 +msgid "Checkout After Creation" +msgstr "Преминаване към клона след създаването му" + +#: lib/branch_create.tcl:132 +msgid "Please select a tracking branch." +msgstr "Изберете клон за следени." + +#: lib/branch_create.tcl:141 +#, tcl-format +msgid "Tracking branch %s is not a branch in the remote repository." +msgstr "Следящият клон — „%s“, не съществува в отдалеченото хранилище." + +#: lib/branch_create.tcl:154 lib/branch_rename.tcl:92 +msgid "Please supply a branch name." +msgstr "Дайте име на клона." + +#: lib/branch_create.tcl:165 lib/branch_rename.tcl:112 +#, tcl-format +msgid "'%s' is not an acceptable branch name." +msgstr "„%s“ не може да се използва за име на клон." + +#: lib/branch_delete.tcl:16 +#, tcl-format +msgid "%s (%s): Delete Branch" +msgstr "%s (%s): Изтриване на клон" + +#: lib/branch_delete.tcl:21 +msgid "Delete Local Branch" +msgstr "Изтриване на локален клон" + +#: lib/branch_delete.tcl:39 +msgid "Local Branches" +msgstr "Локални клони" + +#: lib/branch_delete.tcl:51 +msgid "Delete Only If Merged Into" +msgstr "Изтриване, само ако промените са слети и другаде" + +#: lib/branch_delete.tcl:53 lib/remote_branch_delete.tcl:120 +msgid "Always (Do not perform merge checks)" +msgstr "Винаги (без проверка за сливане)" + +#: lib/branch_delete.tcl:103 +#, tcl-format +msgid "The following branches are not completely merged into %s:" +msgstr "Не всички промени в клоните са слети в „%s“:" + +#: lib/branch_delete.tcl:115 lib/remote_branch_delete.tcl:218 +msgid "" +"Recovering deleted branches is difficult.\n" +"\n" +"Delete the selected branches?" +msgstr "" +"Възстановяването на изтрити клони може да е трудно.\n" +"\n" +"Сигурни ли сте, че искате да триете?" + +#: lib/branch_delete.tcl:131 +#, tcl-format +msgid " - %s:" +msgstr " — „%s:“" + +#: lib/branch_delete.tcl:141 +#, tcl-format +msgid "" +"Failed to delete branches:\n" +"%s" +msgstr "" +"Неуспешно триене на клони:\n" +"%s" + +#: lib/branch_rename.tcl:15 +#, tcl-format +msgid "%s (%s): Rename Branch" +msgstr "%s (%s): Преименуване на клон" + +#: lib/branch_rename.tcl:23 +msgid "Rename Branch" +msgstr "Преименуване на клон" + +#: lib/branch_rename.tcl:28 +msgid "Rename" +msgstr "Преименуване" + +#: lib/branch_rename.tcl:38 +msgid "Branch:" +msgstr "Клон:" + +#: lib/branch_rename.tcl:46 +msgid "New Name:" +msgstr "Ново име:" + +#: lib/branch_rename.tcl:81 +msgid "Please select a branch to rename." +msgstr "Изберете клон за преименуване." + +#: lib/branch_rename.tcl:102 lib/checkout_op.tcl:202 +#, tcl-format +msgid "Branch '%s' already exists." +msgstr "Клонът „%s“ вече съществува." + +#: lib/branch_rename.tcl:123 +#, tcl-format +msgid "Failed to rename '%s'." +msgstr "Неуспешно преименуване на „%s“." + +#: lib/browser.tcl:17 +msgid "Starting..." +msgstr "Стартиране…" + +#: lib/browser.tcl:27 +#, tcl-format +msgid "%s (%s): File Browser" +msgstr "%s (%s): Файлов браузър" + +#: lib/browser.tcl:132 lib/browser.tcl:149 +#, tcl-format +msgid "Loading %s..." +msgstr "Зареждане на „%s“…" + +#: lib/browser.tcl:193 +msgid "[Up To Parent]" +msgstr "[Към родителя]" + +#: lib/browser.tcl:275 +#, tcl-format +msgid "%s (%s): Browse Branch Files" +msgstr "%s (%s): Разглеждане на файловете в клона" + +#: lib/browser.tcl:282 +msgid "Browse Branch Files" +msgstr "Разглеждане на файловете в клона" + +#: lib/browser.tcl:288 lib/choose_repository.tcl:422 +#: lib/choose_repository.tcl:509 lib/choose_repository.tcl:518 +#: lib/choose_repository.tcl:1074 +msgid "Browse" +msgstr "Разглеждане" + +#: lib/checkout_op.tcl:85 +#, tcl-format +msgid "Fetching %s from %s" +msgstr "Доставяне на „%s“ от „%s“" + +#: lib/checkout_op.tcl:133 +#, tcl-format +msgid "fatal: Cannot resolve %s" +msgstr "фатална грешка: „%s“ не може да се открие" + +#: lib/checkout_op.tcl:146 lib/console.tcl:81 lib/database.tcl:30 +#: lib/sshkey.tcl:55 +msgid "Close" +msgstr "Затваряне" + +#: lib/checkout_op.tcl:175 +#, tcl-format +msgid "Branch '%s' does not exist." +msgstr "Клонът „%s“ не съществува." + +#: lib/checkout_op.tcl:194 +#, tcl-format +msgid "Failed to configure simplified git-pull for '%s'." +msgstr "Неуспешно настройване на опростен git-pull за „%s“." + +#: lib/checkout_op.tcl:229 +#, tcl-format +msgid "" +"Branch '%s' already exists.\n" +"\n" +"It cannot fast-forward to %s.\n" +"A merge is required." +msgstr "" +"Клонът „%s“ съществува.\n" +"\n" +"Той не може да бъде тривиално слят до „%s“.\n" +"Необходимо е сливане." + +#: lib/checkout_op.tcl:243 +#, tcl-format +msgid "Merge strategy '%s' not supported." +msgstr "Стратегия за сливане „%s“ не се поддържа." + +#: lib/checkout_op.tcl:262 +#, tcl-format +msgid "Failed to update '%s'." +msgstr "Неуспешно обновяване на „%s“." + +#: lib/checkout_op.tcl:274 +msgid "Staging area (index) is already locked." +msgstr "Индексът вече е заключен." + +#: lib/checkout_op.tcl:289 +msgid "" +"Last scanned state does not match repository state.\n" +"\n" +"Another Git program has modified this repository since the last scan. A " +"rescan must be performed before the current branch can be changed.\n" +"\n" +"The rescan will be automatically started now.\n" +msgstr "" +"Състоянието при последната проверка не отговаря на състоянието на " +"хранилището.\n" +"\n" +"Някой друг процес за Git е променил хранилището междувременно. Състоянието " +"трябва да бъде проверено, преди да се премине към нов клон.\n" +"\n" +"Автоматично ще започне нова проверка.\n" + +#: lib/checkout_op.tcl:345 +#, tcl-format +msgid "Updating working directory to '%s'..." +msgstr "Работната директория се привежда към „%s“…" + +#: lib/checkout_op.tcl:346 +msgid "files checked out" +msgstr "файла са изтеглени" + +#: lib/checkout_op.tcl:376 +#, tcl-format +msgid "Aborted checkout of '%s' (file level merging is required)." +msgstr "" +"Преустановяване на изтеглянето на „%s“ (необходимо е пофайлово сливане)." + +#: lib/checkout_op.tcl:377 +msgid "File level merge required." +msgstr "Необходимо е пофайлово сливане." + +#: lib/checkout_op.tcl:381 +#, tcl-format +msgid "Staying on branch '%s'." +msgstr "Оставане върху клона „%s“." + +#: lib/checkout_op.tcl:452 +msgid "" +"You are no longer on a local branch.\n" +"\n" +"If you wanted to be on a branch, create one now starting from 'This Detached " +"Checkout'." +msgstr "" +"Вече не сте на локален клон.\n" +"\n" +"Ако искате да сте на клон, създайте базиран на „Това несвързано изтегляне“." + +#: lib/checkout_op.tcl:503 lib/checkout_op.tcl:507 +#, tcl-format +msgid "Checked out '%s'." +msgstr "„%s“ е изтеглен." + +#: lib/checkout_op.tcl:535 +#, tcl-format +msgid "Resetting '%s' to '%s' will lose the following commits:" +msgstr "" +"Зануляването на „%s“ към „%s“ ще доведе до загубването на следните подавания:" + +#: lib/checkout_op.tcl:557 +msgid "Recovering lost commits may not be easy." +msgstr "Възстановяването на загубените подавания може да е трудно." + +#: lib/checkout_op.tcl:562 +#, tcl-format +msgid "Reset '%s'?" +msgstr "Зануляване на „%s“?" + +#: lib/checkout_op.tcl:567 lib/merge.tcl:170 lib/tools_dlg.tcl:336 +msgid "Visualize" +msgstr "Визуализация" + +#: lib/checkout_op.tcl:635 +#, tcl-format +msgid "" +"Failed to set current branch.\n" +"\n" +"This working directory is only partially switched. We successfully updated " +"your files, but failed to update an internal Git file.\n" +"\n" +"This should not have occurred. %s will now close and give up." +msgstr "" +"Неуспешно задаване на текущия клон.\n" +"\n" +"Работната директория е само частично обновена: файловете са обновени " +"успешно, но някой от вътрешните, служебни файлове на Git не е бил.\n" +"\n" +"Това състояние е аварийно и не трябва да се случва. Програмата „%s“ ще " +"преустанови работа." + +#: lib/choose_font.tcl:41 +msgid "Select" +msgstr "Избор" + +#: lib/choose_font.tcl:55 +msgid "Font Family" +msgstr "Шрифт" + +#: lib/choose_font.tcl:76 +msgid "Font Size" +msgstr "Размер" + +#: lib/choose_font.tcl:93 +msgid "Font Example" +msgstr "Мостра" + +#: lib/choose_font.tcl:105 +msgid "" +"This is example text.\n" +"If you like this text, it can be your font." +msgstr "" +"Това е примерен текст.\n" +"Ако ви харесва как изглежда, изберете шрифта." + +#: lib/choose_repository.tcl:33 +msgid "Git Gui" +msgstr "ГПИ на Git" + +#: lib/choose_repository.tcl:92 lib/choose_repository.tcl:412 +msgid "Create New Repository" +msgstr "Създаване на ново хранилище" + +#: lib/choose_repository.tcl:98 +msgid "New..." +msgstr "Ново…" + +#: lib/choose_repository.tcl:105 lib/choose_repository.tcl:496 +msgid "Clone Existing Repository" +msgstr "Клониране на съществуващо хранилище" + +#: lib/choose_repository.tcl:116 +msgid "Clone..." +msgstr "Клониране…" + +#: lib/choose_repository.tcl:123 lib/choose_repository.tcl:1064 +msgid "Open Existing Repository" +msgstr "Отваряне на съществуващо хранилище" + +#: lib/choose_repository.tcl:129 +msgid "Open..." +msgstr "Отваряне…" + +#: lib/choose_repository.tcl:142 +msgid "Recent Repositories" +msgstr "Скоро ползвани" + +#: lib/choose_repository.tcl:148 +msgid "Open Recent Repository:" +msgstr "Отваряне на хранилище ползвано наскоро:" + +#: lib/choose_repository.tcl:316 lib/choose_repository.tcl:323 +#: lib/choose_repository.tcl:330 +#, tcl-format +msgid "Failed to create repository %s:" +msgstr "Неуспешно създаване на хранилището „%s“:" + +#: lib/choose_repository.tcl:417 +msgid "Directory:" +msgstr "Директория:" + +#: lib/choose_repository.tcl:447 lib/choose_repository.tcl:573 +#: lib/choose_repository.tcl:1098 +msgid "Git Repository" +msgstr "Хранилище на Git" + +#: lib/choose_repository.tcl:472 +#, tcl-format +msgid "Directory %s already exists." +msgstr "Вече съществува директория „%s“." + +#: lib/choose_repository.tcl:476 +#, tcl-format +msgid "File %s already exists." +msgstr "Вече съществува файл „%s“." + +#: lib/choose_repository.tcl:491 +msgid "Clone" +msgstr "Клониране" + +#: lib/choose_repository.tcl:504 +msgid "Source Location:" +msgstr "Адрес на източника:" + +#: lib/choose_repository.tcl:513 +msgid "Target Directory:" +msgstr "Целева директория:" + +#: lib/choose_repository.tcl:523 +msgid "Clone Type:" +msgstr "Вид клониране:" + +#: lib/choose_repository.tcl:528 +msgid "Standard (Fast, Semi-Redundant, Hardlinks)" +msgstr "Стандартно (бързо, частично споделяне на файлове, твърди връзки)" + +#: lib/choose_repository.tcl:533 +msgid "Full Copy (Slower, Redundant Backup)" +msgstr "Пълно (бавно, пълноценно резервно копие)" + +#: lib/choose_repository.tcl:538 +msgid "Shared (Fastest, Not Recommended, No Backup)" +msgstr "Споделено (най-бързо, не се препоръчва, не прави резервно копие)" + +#: lib/choose_repository.tcl:545 +msgid "Recursively clone submodules too" +msgstr "Рекурсивно клониране и на подмодулите" + +#: lib/choose_repository.tcl:579 lib/choose_repository.tcl:626 +#: lib/choose_repository.tcl:772 lib/choose_repository.tcl:842 +#: lib/choose_repository.tcl:1104 lib/choose_repository.tcl:1112 +#, tcl-format +msgid "Not a Git repository: %s" +msgstr "Това не е хранилище на Git: %s" + +#: lib/choose_repository.tcl:615 +msgid "Standard only available for local repository." +msgstr "Само локални хранилища могат да се клонират стандартно" + +#: lib/choose_repository.tcl:619 +msgid "Shared only available for local repository." +msgstr "Само локални хранилища могат да се клонират споделено" + +#: lib/choose_repository.tcl:640 +#, tcl-format +msgid "Location %s already exists." +msgstr "Местоположението „%s“ вече съществува." + +#: lib/choose_repository.tcl:651 +msgid "Failed to configure origin" +msgstr "Неуспешно настройване на хранилището-източник" + +#: lib/choose_repository.tcl:663 +msgid "Counting objects" +msgstr "Преброяване на обекти" + +#: lib/choose_repository.tcl:664 +msgid "buckets" +msgstr "клетки" + +#: lib/choose_repository.tcl:688 +#, tcl-format +msgid "Unable to copy objects/info/alternates: %s" +msgstr "Обектите/информацията/синонимите не могат да бъдат копирани: %s" + +#: lib/choose_repository.tcl:724 +#, tcl-format +msgid "Nothing to clone from %s." +msgstr "Няма какво да се клонира от „%s“." + +#: lib/choose_repository.tcl:726 lib/choose_repository.tcl:940 +#: lib/choose_repository.tcl:952 +msgid "The 'master' branch has not been initialized." +msgstr "Основният клон — „master“ не е инициализиран." + +#: lib/choose_repository.tcl:739 +msgid "Hardlinks are unavailable. Falling back to copying." +msgstr "Не се поддържат твърди връзки. Преминава се към копиране." + +#: lib/choose_repository.tcl:751 +#, tcl-format +msgid "Cloning from %s" +msgstr "Клониране на „%s“" + +#: lib/choose_repository.tcl:782 +msgid "Copying objects" +msgstr "Копиране на обекти" + +#: lib/choose_repository.tcl:783 +msgid "KiB" +msgstr "KiB" + +#: lib/choose_repository.tcl:807 +#, tcl-format +msgid "Unable to copy object: %s" +msgstr "Неуспешно копиране на обект: %s" + +#: lib/choose_repository.tcl:817 +msgid "Linking objects" +msgstr "Създаване на връзки към обектите" + +#: lib/choose_repository.tcl:818 +msgid "objects" +msgstr "обекти" + +#: lib/choose_repository.tcl:826 +#, tcl-format +msgid "Unable to hardlink object: %s" +msgstr "Неуспешно създаване на твърда връзка към обект: %s" + +#: lib/choose_repository.tcl:881 +msgid "Cannot fetch branches and objects. See console output for details." +msgstr "" +"Клоните и обектите не могат да бъдат изтеглени. За повече информация " +"погледнете изхода на конзолата." + +#: lib/choose_repository.tcl:892 +msgid "Cannot fetch tags. See console output for details." +msgstr "" +"Етикетите не могат да бъдат изтеглени. За повече информация погледнете " +"изхода на конзолата." + +#: lib/choose_repository.tcl:916 +msgid "Cannot determine HEAD. See console output for details." +msgstr "" +"Върхът „HEAD“ не може да бъде определен. За повече информация погледнете " +"изхода на конзолата." + +#: lib/choose_repository.tcl:925 +#, tcl-format +msgid "Unable to cleanup %s" +msgstr "„%s“ не може да се зачисти" + +#: lib/choose_repository.tcl:931 +msgid "Clone failed." +msgstr "Неуспешно клониране." + +#: lib/choose_repository.tcl:938 +msgid "No default branch obtained." +msgstr "Не е получен клон по подразбиране." + +#: lib/choose_repository.tcl:949 +#, tcl-format +msgid "Cannot resolve %s as a commit." +msgstr "Няма подаване отговарящо на „%s“." + +#: lib/choose_repository.tcl:961 +msgid "Creating working directory" +msgstr "Създаване на работната директория" + +#: lib/choose_repository.tcl:962 lib/index.tcl:70 lib/index.tcl:136 +#: lib/index.tcl:207 +msgid "files" +msgstr "файлове" + +#: lib/choose_repository.tcl:981 +msgid "Cannot clone submodules." +msgstr "Подмодулите не могат да се клонират." + +#: lib/choose_repository.tcl:990 +msgid "Cloning submodules" +msgstr "Клониране на подмодули" + +#: lib/choose_repository.tcl:1015 +msgid "Initial file checkout failed." +msgstr "Неуспешно първоначално изтегляне." + +#: lib/choose_repository.tcl:1059 +msgid "Open" +msgstr "Отваряне" + +#: lib/choose_repository.tcl:1069 +msgid "Repository:" +msgstr "Хранилище:" + +#: lib/choose_repository.tcl:1118 +#, tcl-format +msgid "Failed to open repository %s:" +msgstr "Неуспешно отваряне на хранилището „%s“:" + +#: lib/choose_rev.tcl:52 +msgid "This Detached Checkout" +msgstr "Това несвързано изтегляне" + +#: lib/choose_rev.tcl:60 +msgid "Revision Expression:" +msgstr "Израз за версия:" + +#: lib/choose_rev.tcl:72 +msgid "Local Branch" +msgstr "Локален клон" + +#: lib/choose_rev.tcl:77 +msgid "Tracking Branch" +msgstr "Следящ клон" + +#: lib/choose_rev.tcl:82 lib/choose_rev.tcl:544 +msgid "Tag" +msgstr "Етикет" + +#: lib/choose_rev.tcl:321 +#, tcl-format +msgid "Invalid revision: %s" +msgstr "Неправилна версия: %s" + +#: lib/choose_rev.tcl:342 +msgid "No revision selected." +msgstr "Не е избрана версия." + +#: lib/choose_rev.tcl:350 +msgid "Revision expression is empty." +msgstr "Изразът за версия е празен." + +#: lib/choose_rev.tcl:537 +msgid "Updated" +msgstr "Обновен" + +#: lib/choose_rev.tcl:565 +msgid "URL" +msgstr "Адрес" + +#: lib/commit.tcl:9 +msgid "" +"There is nothing to amend.\n" +"\n" +"You are about to create the initial commit. There is no commit before this " +"to amend.\n" +msgstr "" +"Няма какво да се поправи.\n" +"\n" +"Ще създадете първоначалното подаване. Преди него няма други подавания, които " +"да поправите.\n" + +#: lib/commit.tcl:18 +msgid "" +"Cannot amend while merging.\n" +"\n" +"You are currently in the middle of a merge that has not been fully " +"completed. You cannot amend the prior commit unless you first abort the " +"current merge activity.\n" +msgstr "" +"По време на сливане не може да поправяте.\n" +"\n" +"В момента все още не сте завършили операция по сливане. Не може да поправите " +"предишното подаване, освен ако първо не преустановите текущото сливане.\n" + +#: lib/commit.tcl:48 +msgid "Error loading commit data for amend:" +msgstr "Грешка при зареждане на данните от подаване, които да се поправят:" + +#: lib/commit.tcl:75 +msgid "Unable to obtain your identity:" +msgstr "Идентификацията ви не може да бъде определена:" + +#: lib/commit.tcl:80 +msgid "Invalid GIT_COMMITTER_IDENT:" +msgstr "Неправилно поле „GIT_COMMITTER_IDENT“:" + +#: lib/commit.tcl:129 +#, tcl-format +msgid "warning: Tcl does not support encoding '%s'." +msgstr "предупреждение: Tcl не поддържа кодирането „%s“." + +#: lib/commit.tcl:149 +msgid "" +"Last scanned state does not match repository state.\n" +"\n" +"Another Git program has modified this repository since the last scan. A " +"rescan must be performed before another commit can be created.\n" +"\n" +"The rescan will be automatically started now.\n" +msgstr "" +"Състоянието при последната проверка не отговаря на състоянието на " +"хранилището.\n" +"\n" +"Някой друг процес за Git е променил хранилището междувременно. Състоянието " +"трябва да бъде проверено преди ново подаване.\n" +"\n" +"Автоматично ще започне нова проверка.\n" + +#: lib/commit.tcl:173 +#, tcl-format +msgid "" +"Unmerged files cannot be committed.\n" +"\n" +"File %s has merge conflicts. You must resolve them and stage the file " +"before committing.\n" +msgstr "" +"Неслетите файлове не могат да бъдат подавани.\n" +"\n" +"Във файла „%s“ има конфликти при сливане. За да го подадете, трябва първо да " +"коригирате конфликтите и да добавите файла към индекса за подаване.\n" + +#: lib/commit.tcl:181 +#, tcl-format +msgid "" +"Unknown file state %s detected.\n" +"\n" +"File %s cannot be committed by this program.\n" +msgstr "" +"Непознато състояние на файл „%s“.\n" +"\n" +"Файлът „%s“ не може да бъде подаден чрез текущата програма.\n" + +#: lib/commit.tcl:189 +msgid "" +"No changes to commit.\n" +"\n" +"You must stage at least 1 file before you can commit.\n" +msgstr "" +"Няма промени за подаване.\n" +"\n" +"Трябва да добавите поне един файл към индекса, за да подадете.\n" + +#: lib/commit.tcl:204 +msgid "" +"Please supply a commit message.\n" +"\n" +"A good commit message has the following format:\n" +"\n" +"- First line: Describe in one sentence what you did.\n" +"- Second line: Blank\n" +"- Remaining lines: Describe why this change is good.\n" +msgstr "" +"Задайте добро съобщение при подаване.\n" +"\n" +"Използвайте следния формат:\n" +"\n" +"● Първи ред: описание в едно изречение на промяната.\n" +"● Втори ред: празен.\n" +"● Останалите редове: опишете защо се налага тази промяна.\n" + +#: lib/commit.tcl:235 +msgid "Calling pre-commit hook..." +msgstr "Изпълняване на куката преди подаване…" + +#: lib/commit.tcl:250 +msgid "Commit declined by pre-commit hook." +msgstr "Подаването е отхвърлено от куката преди подаване." + +#: lib/commit.tcl:269 +msgid "" +"You are about to commit on a detached head. This is a potentially dangerous " +"thing to do because if you switch to another branch you will lose your " +"changes and it can be difficult to retrieve them later from the reflog. You " +"should probably cancel this commit and create a new branch to continue.\n" +" \n" +" Do you really want to proceed with your Commit?" +msgstr "" +"Ще подадете към несвързан, отделѐн указател „HEAD“. Това е опасно, защото " +"при преминаването към клон ще загубите промените си, като единственият начин " +"да ги върнете ще е чрез журнала на указателите (reflog). Най-вероятно трябва " +"да не правите това подаване, а да създадете нов клон, преди да продължите.\n" +" \n" +"Сигурни ли сте, че искате да извършите текущото подаване?" + +#: lib/commit.tcl:290 +msgid "Calling commit-msg hook..." +msgstr "Изпълняване на куката за съобщението при подаване…" + +#: lib/commit.tcl:305 +msgid "Commit declined by commit-msg hook." +msgstr "Подаването е отхвърлено от куката за съобщението при подаване." + +#: lib/commit.tcl:318 +msgid "Committing changes..." +msgstr "Подаване на промените…" + +#: lib/commit.tcl:334 +msgid "write-tree failed:" +msgstr "неуспешно запазване на дървото (write-tree):" + +#: lib/commit.tcl:335 lib/commit.tcl:382 lib/commit.tcl:403 +msgid "Commit failed." +msgstr "Неуспешно подаване." + +#: lib/commit.tcl:352 +#, tcl-format +msgid "Commit %s appears to be corrupt" +msgstr "Подаването „%s“ изглежда повредено" + +#: lib/commit.tcl:357 +msgid "" +"No changes to commit.\n" +"\n" +"No files were modified by this commit and it was not a merge commit.\n" +"\n" +"A rescan will be automatically started now.\n" +msgstr "" +"Няма промени за подаване.\n" +"\n" +"В това подаване не са променяни никакви файлове, а и не е подаване със " +"сливане.\n" +"\n" +"Автоматично ще започне нова проверка.\n" + +#: lib/commit.tcl:364 +msgid "No changes to commit." +msgstr "Няма промени за подаване." + +#: lib/commit.tcl:381 +msgid "commit-tree failed:" +msgstr "неуспешно подаване на дървото (commit-tree):" + +#: lib/commit.tcl:402 +msgid "update-ref failed:" +msgstr "неуспешно обновяване на указателите (update-ref):" + +#: lib/commit.tcl:495 +#, tcl-format +msgid "Created commit %s: %s" +msgstr "Успешно подаване %s: %s" + +#: lib/console.tcl:59 +msgid "Working... please wait..." +msgstr "В момента се извършва действие, изчакайте…" + +#: lib/console.tcl:186 +msgid "Success" +msgstr "Успех" + +#: lib/console.tcl:200 +msgid "Error: Command Failed" +msgstr "Грешка: неуспешно изпълнение на команда" + +#: lib/database.tcl:42 +msgid "Number of loose objects" +msgstr "Брой непакетирани обекти" + +#: lib/database.tcl:43 +msgid "Disk space used by loose objects" +msgstr "Дисково пространство заето от непакетирани обекти" + +#: lib/database.tcl:44 +msgid "Number of packed objects" +msgstr "Брой пакетирани обекти" + +#: lib/database.tcl:45 |