summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.cirrus.yml15
-rw-r--r--.mailmap1
-rw-r--r--Documentation/RelNotes/2.25.0.txt40
-rw-r--r--Documentation/config/advice.txt2
-rw-r--r--Documentation/git-am.txt4
-rw-r--r--Documentation/git-log.txt8
-rw-r--r--Documentation/git-rebase.txt27
-rw-r--r--Documentation/git-sparse-checkout.txt23
-rw-r--r--Documentation/git-submodule.txt2
-rw-r--r--Documentation/gitk.txt8
-rw-r--r--Documentation/technical/multi-pack-index.txt2
-rwxr-xr-xGIT-VERSION-GEN2
-rw-r--r--builtin/fetch.c4
-rw-r--r--builtin/rebase.c51
-rw-r--r--builtin/sparse-checkout.c21
-rw-r--r--cache.h4
-rw-r--r--commit-graph.c4
-rw-r--r--compat/mingw.c122
-rw-r--r--compat/mingw.h11
-rwxr-xr-xgit-gui/git-gui.sh61
-rw-r--r--git-gui/lib/blame.tcl24
-rw-r--r--git-gui/lib/branch.tcl2
-rw-r--r--git-gui/lib/checkout_op.tcl15
-rw-r--r--git-gui/lib/choose_repository.tcl120
-rw-r--r--git-gui/lib/chord.tcl160
-rw-r--r--git-gui/lib/console.tcl2
-rw-r--r--git-gui/lib/index.tcl523
-rw-r--r--git-gui/lib/merge.tcl14
-rw-r--r--git-gui/lib/status_bar.tcl231
-rwxr-xr-xgit-p4.py74
-rwxr-xr-xgitweb/gitweb.perl4
-rw-r--r--graph.c17
-rw-r--r--merge-recursive.c33
-rw-r--r--packfile.c16
-rw-r--r--read-cache.c7
-rw-r--r--sequencer.c141
-rw-r--r--sequencer.h2
-rwxr-xr-xt/t0060-path-utils.sh13
-rwxr-xr-xt/t1091-sparse-checkout-builtin.sh39
-rwxr-xr-xt/t3008-ls-files-lazy-init-name-hash.sh2
-rwxr-xr-xt/t3422-rebase-incompatible-options.sh2
-rwxr-xr-xt/t3433-rebase-options-compatibility.sh131
-rwxr-xr-xt/t4215-log-skewed-merges.sh71
-rwxr-xr-xt/t7415-submodule-names.sh7
-rw-r--r--tree-walk.c6
45 files changed, 1375 insertions, 693 deletions
diff --git a/.cirrus.yml b/.cirrus.yml
new file mode 100644
index 0000000000..c2f5fe385a
--- /dev/null
+++ b/.cirrus.yml
@@ -0,0 +1,15 @@
+env:
+ CIRRUS_CLONE_DEPTH: 1
+
+freebsd_12_task:
+ freebsd_instance:
+ image: freebsd-12-1-release-amd64
+ install_script:
+ pkg install -y gettext gmake perl5
+ create_user_script:
+ - pw useradd git
+ - chown -R git:git .
+ build_script:
+ - su git -c gmake
+ test_script:
+ - su git -c 'gmake test'
diff --git a/.mailmap b/.mailmap
index 14fa041043..7c9441837a 100644
--- a/.mailmap
+++ b/.mailmap
@@ -60,6 +60,7 @@ David Turner <novalis@novalis.org> <dturner@twopensource.com>
David Turner <novalis@novalis.org> <dturner@twosigma.com>
Derrick Stolee <dstolee@microsoft.com> <stolee@gmail.com>
Deskin Miller <deskinm@umich.edu>
+Đoàn Trần Công Danh <congdanhqx@gmail.com> Doan Tran Cong Danh
Dirk Süsserott <newsletter@dirk.my1.cc>
Eric Blake <eblake@redhat.com> <ebb9@byu.net>
Eric Hanchrow <eric.hanchrow@gmail.com> <offby1@blarg.net>
diff --git a/Documentation/RelNotes/2.25.0.txt b/Documentation/RelNotes/2.25.0.txt
index 8956061c72..91ceb34927 100644
--- a/Documentation/RelNotes/2.25.0.txt
+++ b/Documentation/RelNotes/2.25.0.txt
@@ -53,9 +53,6 @@ UI, Workflows & Features
or a named file, instead of taking it as the command line
arguments, with the "--pathspec-from-file" option.
- * "git rebase -i" learned a few options that are known by "git
- rebase" proper.
-
* "git submodule" learned a subcommand "set-url".
* "git log" family learned "--pretty=reference" that gives the name
@@ -70,6 +67,11 @@ UI, Workflows & Features
* Management of sparsely checked-out working tree has gained a
dedicated "sparse-checkout" command.
+ * Miscellaneous small UX improvements on "git-p4".
+
+ * "git sparse-checkout list" subcommand learned to give its output in
+ a more concise form when the "cone" mode is in effect.
+
Performance, Internal Implementation, Development Support etc.
@@ -141,14 +143,14 @@ Performance, Internal Implementation, Development Support etc.
* The code has been made to avoid gmtime() and localtime() and prefer
their reentrant counterparts.
- * The effort to reimplement "git add -i" in C continues.
-
* In a repository with many packfiles, the cost of the procedure that
avoids registering the same packfile twice was unnecessarily high
by using an inefficient search algorithm, which has been corrected.
* Redo "git name-rev" to avoid recursive calls.
+ * FreeBSD CI support via Cirrus-CI has been added.
+
Fixes since v2.24
-----------------
@@ -303,6 +305,26 @@ Fixes since v2.24
* Assorted fixes to the directory traversal API.
(merge 6836d2fe06 en/fill-directory-fixes later to maint).
+ * Forbid pathnames that the platform's filesystem cannot represent on
+ MinGW.
+ (merge 4dc42c6c18 js/mingw-reserved-filenames later to maint).
+
+ * "git rebase --signoff" stopped working when the command was written
+ in C, which has been corrected.
+ (merge 4fe7e43c53 en/rebase-signoff-fix later to maint).
+
+ * An earlier update to Git for Windows declared that a tree object is
+ invalid if it has a path component with backslash in it, which was
+ overly strict, which has been corrected. The only protection the
+ Windows users need is to prevent such path (or any path that their
+ filesystem cannot check out) from entering the index.
+ (merge 224c7d70fa js/mingw-loosen-overstrict-tree-entry-checks later to maint).
+
+ * The code to write split commit-graph file(s) upon fetching computed
+ bogus value for the parameter used in splitting the resulting
+ files, which has been corrected.
+ (merge 63020f175f ds/commit-graph-set-size-mult later to maint).
+
* Other code cleanup, docfix, build fix, etc.
(merge 80736d7c5e jc/am-show-current-patch-docfix later to maint).
(merge 8b656572ca sg/commit-graph-usage-fix later to maint).
@@ -338,3 +360,11 @@ Fixes since v2.24
(merge cc2bd5c45d pb/submodule-doc-xref later to maint).
(merge df5be01669 ja/doc-markup-cleanup later to maint).
(merge 7c5cea7242 mr/bisect-save-pointer-to-const-string later to maint).
+ (merge 20a67e8ce9 js/use-test-tool-on-path later to maint).
+ (merge 4e61b2214d ew/packfile-syscall-optim later to maint).
+ (merge ace0f86c7f pb/clarify-line-log-doc later to maint).
+ (merge 763a59e71c en/merge-recursive-oid-eq-simplify later to maint).
+ (merge 4e2c4c0d4f do/gitweb-typofix-in-comments later to maint).
+ (merge 421c0ffb02 jb/doc-multi-pack-idx-fix later to maint).
+ (merge f8740c586b pm/am-in-body-header-doc-update later to maint).
+ (merge 5814d44d9b tm/doc-submodule-absorb-fix later to maint).
diff --git a/Documentation/config/advice.txt b/Documentation/config/advice.txt
index d4e698cd3f..4be93f8ad9 100644
--- a/Documentation/config/advice.txt
+++ b/Documentation/config/advice.txt
@@ -107,7 +107,7 @@ advice.*::
editor input from the user.
nestedTag::
Advice shown if a user attempts to recursively tag a tag object.
- submoduleAlternateErrorStrategyDie:
+ submoduleAlternateErrorStrategyDie::
Advice shown when a submodule.alternateErrorStrategy option
configured to "die" causes a fatal error.
--
diff --git a/Documentation/git-am.txt b/Documentation/git-am.txt
index fc5750b3b8..11ca61b00b 100644
--- a/Documentation/git-am.txt
+++ b/Documentation/git-am.txt
@@ -190,8 +190,8 @@ the commit, after stripping common prefix "[PATCH <anything>]".
The "Subject: " line is supposed to concisely describe what the
commit is about in one line of text.
-"From: " and "Subject: " lines starting the body override the respective
-commit author name and title values taken from the headers.
+"From: ", "Date: ", and "Subject: " lines starting the body override the
+respective commit author name and title values taken from the headers.
The commit message is formed by the title taken from the
"Subject: ", a blank line and the body of the message up to
diff --git a/Documentation/git-log.txt b/Documentation/git-log.txt
index b406bc4c48..bed09bb09e 100644
--- a/Documentation/git-log.txt
+++ b/Documentation/git-log.txt
@@ -76,8 +76,12 @@ produced by `--stat`, etc.
(or the function name regex <funcname>) within the <file>. You may
not give any pathspec limiters. This is currently limited to
a walk starting from a single revision, i.e., you may only
- give zero or one positive revision arguments.
- You can specify this option more than once.
+ give zero or one positive revision arguments, and
+ <start> and <end> (or <funcname>) must exist in the starting revision.
+ You can specify this option more than once. Implies `--patch`.
+ Patch output can be suppressed using `--no-patch`, but other diff formats
+ (namely `--raw`, `--numstat`, `--shortstat`, `--dirstat`, `--summary`,
+ `--name-only`, `--name-status`, `--check`) are not currently implemented.
+
include::line-range-format.txt[]
diff --git a/Documentation/git-rebase.txt b/Documentation/git-rebase.txt
index 1d0e2d27cc..0c4f038dd6 100644
--- a/Documentation/git-rebase.txt
+++ b/Documentation/git-rebase.txt
@@ -393,31 +393,16 @@ your branch contains commits which were dropped, this option can be used
with `--keep-base` in order to drop those commits from your branch.
--ignore-whitespace::
- Behaves differently depending on which backend is selected.
-+
-'am' backend: When applying a patch, ignore changes in whitespace in
-context lines if necessary.
-+
-'interactive' backend: Treat lines with only whitespace changes as
-unchanged for the sake of a three-way merge.
-
--whitespace=<option>::
- This flag is passed to the 'git apply' program
+ These flag are passed to the 'git apply' program
(see linkgit:git-apply[1]) that applies the patch.
+
See also INCOMPATIBLE OPTIONS below.
--committer-date-is-author-date::
- Instead of recording the time the rebased commits are
- created as the committer date, reuse the author date
- as the committer date. This implies --force-rebase.
-
--ignore-date::
---reset-author-date::
- By default, the author date of the original commit is used
- as the author date for the resulting commit. This option
- tells Git to use the current timestamp instead and implies
- `--force-rebase`.
+ These flags are passed to 'git am' to easily change the dates
+ of the rebased commits (see linkgit:git-am[1]).
+
See also INCOMPATIBLE OPTIONS below.
@@ -554,7 +539,10 @@ INCOMPATIBLE OPTIONS
The following options:
+ * --committer-date-is-author-date
+ * --ignore-date
* --whitespace
+ * --ignore-whitespace
* -C
are incompatible with the following options:
@@ -577,9 +565,6 @@ In addition, the following pairs of options are incompatible:
* --preserve-merges and --interactive
* --preserve-merges and --signoff
* --preserve-merges and --rebase-merges
- * --preserve-merges and --ignore-whitespace
- * --preserve-merges and --committer-date-is-author-date
- * --preserve-merges and --ignore-date
* --keep-base and --onto
* --keep-base and --root
diff --git a/Documentation/git-sparse-checkout.txt b/Documentation/git-sparse-checkout.txt
index 9c3c66cc37..974ade2238 100644
--- a/Documentation/git-sparse-checkout.txt
+++ b/Documentation/git-sparse-checkout.txt
@@ -5,7 +5,7 @@ NAME
----
git-sparse-checkout - Initialize and modify the sparse-checkout
configuration, which reduces the checkout to a set of paths
-given by a list of atterns.
+given by a list of patterns.
SYNOPSIS
@@ -28,7 +28,7 @@ THE FUTURE.
COMMANDS
--------
'list'::
- Provide a list of the contents in the sparse-checkout file.
+ Describe the patterns in the sparse-checkout file.
'init'::
Enable the `core.sparseCheckout` setting. If the
@@ -150,11 +150,30 @@ expecting patterns of these types. Git will warn if the patterns do not match.
If the patterns do match the expected format, then Git will use faster hash-
based algorithms to compute inclusion in the sparse-checkout.
+In the cone mode case, the `git sparse-checkout list` subcommand will list the
+directories that define the recursive patterns. For the example sparse-checkout
+file above, the output is as follows:
+
+--------------------------
+$ git sparse-checkout list
+A/B/C
+--------------------------
+
If `core.ignoreCase=true`, then the pattern-matching algorithm will use a
case-insensitive check. This corrects for case mismatched filenames in the
'git sparse-checkout set' command to reflect the expected cone in the working
directory.
+
+SUBMODULES
+----------
+
+If your repository contains one or more submodules, then those submodules will
+appear based on which you initialized with the `git submodule` command. If
+your sparse-checkout patterns exclude an initialized submodule, then that
+submodule will still appear in your working directory.
+
+
SEE ALSO
--------
diff --git a/Documentation/git-submodule.txt b/Documentation/git-submodule.txt
index 22425cbc76..5232407f68 100644
--- a/Documentation/git-submodule.txt
+++ b/Documentation/git-submodule.txt
@@ -248,7 +248,7 @@ registered submodules, and sync any nested submodules within.
absorbgitdirs::
If a git directory of a submodule is inside the submodule,
- move the git directory of the submodule into its superprojects
+ move the git directory of the submodule into its superproject's
`$GIT_DIR/modules` path and then connect the git directory and
its working directory by setting the `core.worktree` and adding
a .git file pointing to the git directory embedded in the
diff --git a/Documentation/gitk.txt b/Documentation/gitk.txt
index 1eabb0aaf3..c653ebb6a8 100644
--- a/Documentation/gitk.txt
+++ b/Documentation/gitk.txt
@@ -105,8 +105,12 @@ linkgit:git-rev-list[1] for a complete list.
(or the function name regex <funcname>) within the <file>. You may
not give any pathspec limiters. This is currently limited to
a walk starting from a single revision, i.e., you may only
- give zero or one positive revision arguments.
- You can specify this option more than once.
+ give zero or one positive revision arguments, and
+ <start> and <end> (or <funcname>) must exist in the starting revision.
+ You can specify this option more than once. Implies `--patch`.
+ Patch output can be suppressed using `--no-patch`, but other diff formats
+ (namely `--raw`, `--numstat`, `--shortstat`, `--dirstat`, `--summary`,
+ `--name-only`, `--name-status`, `--check`) are not currently implemented.
+
*Note:* gitk (unlike linkgit:git-log[1]) currently only understands
this option if you specify it "glued together" with its argument. Do
diff --git a/Documentation/technical/multi-pack-index.txt b/Documentation/technical/multi-pack-index.txt
index 1e31239696..4e7631437a 100644
--- a/Documentation/technical/multi-pack-index.txt
+++ b/Documentation/technical/multi-pack-index.txt
@@ -36,7 +36,7 @@ Design Details
directory of an alternate. It refers only to packfiles in that
same directory.
-- The pack.multiIndex config setting must be on to consume MIDX files.
+- The core.multiPackIndex config setting must be on to consume MIDX files.
- The file format includes parameters for the object ID hash
function, so a future change of hash algorithm does not require
diff --git a/GIT-VERSION-GEN b/GIT-VERSION-GEN
index 4ad93d8be2..2e0ddec5b3 100755
--- a/GIT-VERSION-GEN
+++ b/GIT-VERSION-GEN
@@ -1,7 +1,7 @@
#!/bin/sh
GVF=GIT-VERSION-FILE
-DEF_VER=v2.25.0-rc0
+DEF_VER=v2.25.0-rc2
LF='
'
diff --git a/builtin/fetch.c b/builtin/fetch.c
index f8765b385b..b4c6d921d0 100644
--- a/builtin/fetch.c
+++ b/builtin/fetch.c
@@ -1866,15 +1866,13 @@ int cmd_fetch(int argc, const char **argv, const char *prefix)
(fetch_write_commit_graph < 0 &&
the_repository->settings.fetch_write_commit_graph)) {
int commit_graph_flags = COMMIT_GRAPH_WRITE_SPLIT;
- struct split_commit_graph_opts split_opts;
- memset(&split_opts, 0, sizeof(struct split_commit_graph_opts));
if (progress)
commit_graph_flags |= COMMIT_GRAPH_WRITE_PROGRESS;
write_commit_graph_reachable(get_object_directory(),
commit_graph_flags,
- &split_opts);
+ NULL);
}
close_object_store(the_repository->objects);
diff --git a/builtin/rebase.c b/builtin/rebase.c
index ddf33bc9d4..8081741f8a 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -79,11 +79,8 @@ struct rebase_options {
int allow_rerere_autoupdate;
int keep_empty;
int autosquash;
- int ignore_whitespace;
char *gpg_sign_opt;
int autostash;
- int committer_date_is_author_date;
- int ignore_date;
char *cmd;
int allow_empty_message;
int rebase_merges, rebase_cousins;
@@ -102,7 +99,6 @@ struct rebase_options {
static struct replay_opts get_replay_opts(const struct rebase_options *opts)
{
- struct strbuf strategy_buf = STRBUF_INIT;
struct replay_opts replay = REPLAY_OPTS_INIT;
replay.action = REPLAY_INTERACTIVE_REBASE;
@@ -116,20 +112,10 @@ static struct replay_opts get_replay_opts(const struct rebase_options *opts)
replay.allow_empty_message = opts->allow_empty_message;
replay.verbose = opts->flags & REBASE_VERBOSE;
replay.reschedule_failed_exec = opts->reschedule_failed_exec;
- replay.committer_date_is_author_date =
- opts->committer_date_is_author_date;
- replay.ignore_date = opts->ignore_date;
replay.gpg_sign = xstrdup_or_null(opts->gpg_sign_opt);
replay.strategy = opts->strategy;
-
if (opts->strategy_opts)
- strbuf_addstr(&strategy_buf, opts->strategy_opts);
- if (opts->ignore_whitespace)
- strbuf_addstr(&strategy_buf, " --ignore-space-change");
- if (strategy_buf.len)
- parse_strategy_opts(&replay, strategy_buf.buf);
-
- strbuf_release(&strategy_buf);
+ parse_strategy_opts(&replay, opts->strategy_opts);
if (opts->squash_onto) {
oidcpy(&replay.squash_onto, opts->squash_onto);
@@ -531,8 +517,6 @@ int cmd_rebase__interactive(int argc, const char **argv, const char *prefix)
argc = parse_options(argc, argv, prefix, options,
builtin_rebase_interactive_usage, PARSE_OPT_KEEP_ARGV0);
- opts.strategy_opts = xstrdup_or_null(opts.strategy_opts);
-
if (!is_null_oid(&squash_onto))
opts.squash_onto = &squash_onto;
@@ -706,7 +690,7 @@ static int rebase_write_basic_state(struct rebase_options *opts)
write_file(state_dir_path("gpg_sign_opt", opts), "%s",
opts->gpg_sign_opt);
if (opts->signoff)
- write_file(state_dir_path("strategy", opts), "--signoff");
+ write_file(state_dir_path("signoff", opts), "--signoff");
return 0;
}
@@ -986,12 +970,6 @@ static int run_am(struct rebase_options *opts)
am.git_cmd = 1;
argv_array_push(&am.args, "am");
- if (opts->ignore_whitespace)
- argv_array_push(&am.args, "--ignore-whitespace");
- if (opts->committer_date_is_author_date)
- argv_array_push(&opts->git_am_opts, "--committer-date-is-author-date");
- if (opts->ignore_date)
- argv_array_push(&opts->git_am_opts, "--ignore-date");
if (opts->action && !strcmp("continue", opts->action)) {
argv_array_push(&am.args, "--resolved");
argv_array_pushf(&am.args, "--resolvemsg=%s", resolvemsg);
@@ -1459,17 +1437,16 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
PARSE_OPT_NOARG, NULL, REBASE_DIFFSTAT },
OPT_BOOL(0, "signoff", &options.signoff,
N_("add a Signed-off-by: line to each commit")),
- OPT_BOOL(0, "committer-date-is-author-date",
- &options.committer_date_is_author_date,
- N_("make committer date match author date")),
- OPT_BOOL(0, "reset-author-date", &options.ignore_date,
- N_("ignore author date and use current date")),
- OPT_HIDDEN_BOOL(0, "ignore-date", &options.ignore_date,
- N_("synonym of --reset-author-date")),
+ OPT_PASSTHRU_ARGV(0, "ignore-whitespace", &options.git_am_opts,
+ NULL, N_("passed to 'git am'"),
+ PARSE_OPT_NOARG),
+ OPT_PASSTHRU_ARGV(0, "committer-date-is-author-date",
+ &options.git_am_opts, NULL,
+ N_("passed to 'git am'"), PARSE_OPT_NOARG),
+ OPT_PASSTHRU_ARGV(0, "ignore-date", &options.git_am_opts, NULL,
+ N_("passed to 'git am'"), PARSE_OPT_NOARG),
OPT_PASSTHRU_ARGV('C', NULL, &options.git_am_opts, N_("n"),
N_("passed to 'git apply'"), 0),
- OPT_BOOL(0, "ignore-whitespace", &options.ignore_whitespace,
- N_("ignore changes in whitespace")),
OPT_PASSTHRU_ARGV(0, "whitespace", &options.git_am_opts,
N_("action"), N_("passed to 'git apply'"), 0),
OPT_BIT('f', "force-rebase", &options.flags,
@@ -1742,13 +1719,11 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
state_dir_base, cmd_live_rebase, buf.buf);
}
- if (options.committer_date_is_author_date ||
- options.ignore_date)
- options.flags |= REBASE_FORCE;
-
for (i = 0; i < options.git_am_opts.argc; i++) {
const char *option = options.git_am_opts.argv[i], *p;
- if (!strcmp(option, "--whitespace=fix") ||
+ if (!strcmp(option, "--committer-date-is-author-date") ||
+ !strcmp(option, "--ignore-date") ||
+ !strcmp(option, "--whitespace=fix") ||
!strcmp(option, "--whitespace=strip"))
options.flags |= REBASE_FORCE;
else if (skip_prefix(option, "-C", &p)) {
diff --git a/builtin/sparse-checkout.c b/builtin/sparse-checkout.c
index 5d62f7a66d..b3bed891cb 100644
--- a/builtin/sparse-checkout.c
+++ b/builtin/sparse-checkout.c
@@ -53,6 +53,8 @@ static int sparse_checkout_list(int argc, const char **argv)
memset(&pl, 0, sizeof(pl));
+ pl.use_cone_patterns = core_sparse_checkout_cone;
+
sparse_filename = get_sparse_checkout_filename();
res = add_patterns_from_file_to_list(sparse_filename, "", 0, &pl, NULL);
free(sparse_filename);
@@ -62,6 +64,25 @@ static int sparse_checkout_list(int argc, const char **argv)
return 0;
}
+ if (pl.use_cone_patterns) {
+ int i;
+ struct pattern_entry *pe;
+ struct hashmap_iter iter;
+ struct string_list sl = STRING_LIST_INIT_DUP;
+
+ hashmap_for_each_entry(&pl.recursive_hashmap, &iter, pe, ent) {
+ /* pe->pattern starts with "/", skip it */
+ string_list_insert(&sl, pe->pattern + 1);
+ }
+
+ string_list_sort(&sl);
+
+ for (i = 0; i < sl.nr; i++)
+ printf("%s\n", sl.items[i].string);
+
+ return 0;
+ }
+
write_patterns_to_file(stdout, &pl);
clear_pattern_list(&pl);
diff --git a/cache.h b/cache.h
index 1554488d66..cbfaead23a 100644
--- a/cache.h
+++ b/cache.h
@@ -958,8 +958,8 @@ extern int protect_hfs;
extern int protect_ntfs;
extern const char *core_fsmonitor;
-int core_apply_sparse_checkout;
-int core_sparse_checkout_cone;
+extern int core_apply_sparse_checkout;
+extern int core_sparse_checkout_cone;
/*
* Include broken refs in all ref iterations, which will
diff --git a/commit-graph.c b/commit-graph.c
index e771394aff..b205e65ed1 100644
--- a/commit-graph.c
+++ b/commit-graph.c
@@ -1542,7 +1542,9 @@ static void split_graph_merge_strategy(struct write_commit_graph_context *ctx)
if (ctx->split_opts) {
max_commits = ctx->split_opts->max_commits;
- size_mult = ctx->split_opts->size_multiple;
+
+ if (ctx->split_opts->size_multiple)
+ size_mult = ctx->split_opts->size_multiple;
}
g = ctx->r->objects->commit_graph;
diff --git a/compat/mingw.c b/compat/mingw.c
index 76ac8713d2..402c1ad91c 100644
--- a/compat/mingw.c
+++ b/compat/mingw.c
@@ -404,7 +404,7 @@ int mingw_mkdir(const char *path, int mode)
int ret;
wchar_t wpath[MAX_PATH];
- if (!is_valid_win32_path(path)) {
+ if (!is_valid_win32_path(path, 0)) {
errno = EINVAL;
return -1;
}
@@ -490,21 +490,21 @@ int mingw_open (const char *filename, int oflags, ...)
mode = va_arg(args, int);
va_end(args);
- if (!is_valid_win32_path(filename)) {
+ if (!is_valid_win32_path(filename, !create)) {
errno = create ? EINVAL : ENOENT;
return -1;
}
- if (filename && !strcmp(filename, "/dev/null"))
- filename = "nul";
-
if ((oflags & O_APPEND) && !is_local_named_pipe_path(filename))
open_fn = mingw_open_append;
else
open_fn = _wopen;
- if (xutftowcs_path(wfilename, filename) < 0)
+ if (filename && !strcmp(filename, "/dev/null"))
+ wcscpy(wfilename, L"nul");
+ else if (xutftowcs_path(wfilename, filename) < 0)
return -1;
+
fd = open_fn(wfilename, oflags, mode);
if (fd < 0 && (oflags & O_ACCMODE) != O_RDONLY && errno == EACCES) {
@@ -561,16 +561,18 @@ FILE *mingw_fopen (const char *filename, const char *otype)
int hide = needs_hiding(filename);
FILE *file;
wchar_t wfilename[MAX_PATH], wotype[4];
- if (!is_valid_win32_path(filename)) {
+ if (filename && !strcmp(filename, "/dev/null"))
+ wcscpy(wfilename, L"nul");
+ else if (!is_valid_win32_path(filename, 1)) {
int create = otype && strchr(otype, 'w');
errno = create ? EINVAL : ENOENT;
return NULL;
- }
- if (filename && !strcmp(filename, "/dev/null"))
- filename = "nul";
- if (xutftowcs_path(wfilename, filename) < 0 ||
- xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
+ } else if (xutftowcs_path(wfilename, filename) < 0)
return NULL;
+
+ if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
+ return NULL;
+
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
error("could not unhide %s", filename);
return NULL;
@@ -588,16 +590,18 @@ FILE *mingw_freopen (const char *filename, const char *otype, FILE *stream)
int hide = needs_hiding(filename);
FILE *file;
wchar_t wfilename[MAX_PATH], wotype[4];
- if (!is_valid_win32_path(filename)) {
+ if (filename && !strcmp(filename, "/dev/null"))
+ wcscpy(wfilename, L"nul");
+ else if (!is_valid_win32_path(filename, 1)) {
int create = otype && strchr(otype, 'w');
errno = create ? EINVAL : ENOENT;
return NULL;
- }
- if (filename && !strcmp(filename, "/dev/null"))
- filename = "nul";
- if (xutftowcs_path(wfilename, filename) < 0 ||
- xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
+ } else if (xutftowcs_path(wfilename, filename) < 0)
+ return NULL;
+
+ if (xutftowcs(wotype, otype, ARRAY_SIZE(wotype)) < 0)
return NULL;
+
if (hide && !access(filename, F_OK) && set_hidden_flag(wfilename, 0)) {
error("could not unhide %s", filename);
return NULL;
@@ -2529,14 +2533,16 @@ static void setup_windows_environment(void)
}
}
-int is_valid_win32_path(const char *path)
+int is_valid_win32_path(const char *path, int allow_literal_nul)
{
+ const char *p = path;
int preceding_space_or_period = 0, i = 0, periods = 0;
if (!protect_ntfs)
return 1;
skip_dos_drive_prefix((char **)&path);
+ goto segment_start;
for (;;) {
char c = *(path++);
@@ -2551,7 +2557,83 @@ int is_valid_win32_path(const char *path)
return 1;
i = periods = preceding_space_or_period = 0;
- continue;
+
+segment_start:
+ switch (*path) {
+ case 'a': case 'A': /* AUX */
+ if (((c = path[++i]) != 'u' && c != 'U') ||
+ ((c = path[++i]) != 'x' && c != 'X')) {
+not_a_reserved_name:
+ path += i;
+ continue;
+ }
+ break;
+ case 'c': case 'C': /* COM<N>, CON, CONIN$, CONOUT$ */
+ if ((c = path[++i]) != 'o' && c != 'O')
+ goto not_a_reserved_name;
+ c = path[++i];
+ if (c == 'm' || c == 'M') { /* COM<N> */
+ if (!isdigit(path[++i]))
+ goto not_a_reserved_name;
+ } else if (c == 'n' || c == 'N') { /* CON */
+ c = path[i + 1];
+ if ((c == 'i' || c == 'I') &&
+ ((c = path[i + 2]) == 'n' ||
+ c == 'N') &&
+ path[i + 3] == '$')
+ i += 3; /* CONIN$ */
+ else if ((c == 'o' || c == 'O') &&
+ ((c = path[i + 2]) == 'u' ||
+ c == 'U') &&
+ ((c = path[i + 3]) == 't' ||
+ c == 'T') &&
+ path[i + 4] == '$')
+ i += 4; /* CONOUT$ */
+ } else
+ goto not_a_reserved_name;
+ break;
+ case 'l': case 'L': /* LPT<N> */
+ if (((c = path[++i]) != 'p' && c != 'P') ||
+ ((c = path[++i]) != 't' && c != 'T') ||
+ !isdigit(path[++i]))
+ goto not_a_reserved_name;
+ break;
+ case 'n': case 'N': /* NUL */
+ if (((c = path[++i]) != 'u' && c != 'U') ||
+ ((c = path[++i]) != 'l' && c != 'L') ||
+ (allow_literal_nul &&
+ !path[i + 1] && p == path))
+ goto not_a_reserved_name;
+ break;
+ case 'p': case 'P': /* PRN */
+ if (((c = path[++i]) != 'r' && c != 'R') ||
+ ((c = path[++i]) != 'n' && c != 'N'))
+ goto not_a_reserved_name;
+ break;
+ default:
+ continue;
+ }
+
+ /*
+ * So far, this looks like a reserved name. Let's see
+ * whether it actually is one: trailing spaces, a file
+ * extension, or an NTFS Alternate Data Stream do not
+ * matter, the name is still reserved if any of those
+ * follow immediately after the actual name.
+ */
+ i++;
+ if (path[i] == ' ') {
+ preceding_space_or_period = 1;
+ while (path[++i] == ' ')
+ ; /* skip all spaces */
+ }
+
+ c = path[i];
+ if (c && c != '.' && c != ':' && c != '/' && c != '\\')
+ goto not_a_reserved_name;
+
+ /* contains reserved name */
+ return 0;
case '.':
periods++;
/* fallthru */
diff --git a/compat/mingw.h b/compat/mingw.h
index 3ec9fc36a5..714bc1d591 100644
--- a/compat/mingw.h
+++ b/compat/mingw.h
@@ -461,10 +461,17 @@ char *mingw_query_user_email(void);
*
* - contain any of the reserved characters, e.g. `:`, `;`, `*`, etc
*
+ * - correspond to reserved names (such as `AUX`, `PRN`, etc)
+ *
+ * The `allow_literal_nul` parameter controls whether the path `NUL` should
+ * be considered valid (this makes sense e.g. before opening files, as it is
+ * perfectly legitimate to open `NUL` on Windows, just as it is to open
+ * `/dev/null` on Unix/Linux).
+ *
* Returns 1 upon success, otherwise 0.
*/
-int is_valid_win32_path(const char *path);
-#define is_valid_path(path) is_valid_win32_path(path)
+int is_valid_win32_path(const char *path, int allow_literal_nul);
+#define is_valid_path(path) is_valid_win32_path(path, 0)
/**
* Converts UTF-8 encoded string to UTF-16LE.
diff --git a/git-gui/git-gui.sh b/git-gui/git-gui.sh
index 0d21f5688b..f41ed2eb20 100755
--- a/git-gui/git-gui.sh
+++ b/git-gui/git-gui.sh
@@ -30,8 +30,8 @@ along with this program; if not, see <http://www.gnu.org/licenses/>.}]
##
## Tcl/Tk sanity check
-if {[catch {package require Tcl 8.4} err]
- || [catch {package require Tk 8.4} err]
+if {[catch {package require Tcl 8.6} err]
+ || [catch {package require Tk 8.6} err]
} {
catch {wm withdraw .}
tk_messageBox \
@@ -684,6 +684,7 @@ 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 {}
}
@@ -1797,10 +1798,10 @@ proc ui_status {msg} {
}
}
-proc ui_ready {{test {}}} {
+proc ui_ready {} {
global main_status
if {[info exists main_status]} {
- $main_status show [mc "Ready."] $test
+ $main_status show [mc "Ready."]
}
}
@@ -2150,8 +2151,6 @@ proc incr_font_size {font {amt 1}} {
##
## ui commands
-set starting_gitk_msg [mc "Starting gitk... please wait..."]
-
proc do_gitk {revs {is_submodule false}} {
global current_diff_path file_states current_diff_side ui_index
global _gitdir _gitworktree
@@ -2206,10 +2205,11 @@ proc do_gitk {revs {is_submodule false}} {
set env(GIT_WORK_TREE) $_gitworktree
cd $pwd
- ui_status $::starting_gitk_msg
- after 10000 {
- ui_ready $starting_gitk_msg
- }
+ set status_operation [$::main_status \
+ start \
+ [mc "Starting %s... please wait..." "gitk"]]
+
+ after 3500 [list $status_operation stop]
}
}
@@ -2240,16 +2240,16 @@ proc do_git_gui {} {
set env(GIT_WORK_TREE) $_gitworktree
cd $pwd
- ui_status $::starting_gitk_msg
- after 10000 {
- ui_ready $starting_gitk_msg
- }
+ set status_operation [$::main_status \
+ start \
+ [mc "Starting %s... please wait..." "git-gui"]]
+
+ after 3500 [list $status_operation stop]
}
}
-proc do_explore {} {
- global _gitworktree
- set explorer {}
+# Get the system-specific explorer app/command.
+proc get_explorer {} {
if {[is_Cygwin] || [is_Windows]} {
set explorer "explorer.exe"
} elseif {[is_MacOSX]} {
@@ -2258,9 +2258,23 @@ proc do_explore {} {
# 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
@@ -3512,9 +3526,11 @@ tlabel .vpane.lower.diff.header.file \
-justify left
tlabel .vpane.lower.diff.header.path \
-background gold \
- -foreground black \
+ -foreground blue \
-anchor w \
- -justify left
+ -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
@@ -3529,8 +3545,12 @@ $ctxm add command \
-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
#
@@ -4159,6 +4179,9 @@ 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
diff --git a/git-gui/lib/blame.tcl b/git-gui/lib/blame.tcl
index a1aeb8b96e..62ec083667 100644
--- a/git-gui/lib/blame.tcl
+++ b/git-gui/lib/blame.tcl
@@ -24,6 +24,7 @@ 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
@@ -274,6 +275,7 @@ constructor new {i_commit i_path i_jump} {
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 \
@@ -602,16 +604,23 @@ method _exec_blame {cur_w cur_d options cur_s} {
} 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
-
- $status start \
- $cur_s \
- [mc "lines annotated"]
}
method _read_blame {fd cur_w cur_d} {
@@ -806,10 +815,11 @@ method _read_blame {fd cur_w cur_d} {
[mc "Loading original location annotations..."]
} else {
set current_fd {}
- $status stop [mc "Annotation complete."]
+ $status_operation stop [mc "Annotation complete."]
+ set status_operation {}
}
} else {
- $status update $blame_lines $total_lines
+ $status_operation update $blame_lines $total_lines
}
} ifdeleted { catch {close $fd} }
@@ -1124,7 +1134,7 @@ method _blameparent {} {
set diffcmd [list diff-tree --unified=0 $cparent $cmit -- $new_path]
}
if {[catch {set fd [eval git_read $diffcmd]} err]} {
- $status stop [mc "Unable to display parent"]
+ $status_operation stop [mc "Unable to display parent"]
error_popup [strcat [mc "Error loading diff:"] "\n\n$err"]
return
}
diff --git a/git-gui/lib/branch.tcl b/git-gui/lib/branch.tcl
index 777eeb79c1..8b0c485889 100644
--- a/git-gui/lib/branch.tcl
+++ b/git-gui/lib/branch.tcl
@@ -8,6 +8,7 @@ proc load_all_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]
@@ -24,6 +25,7 @@ proc load_all_tags {} {
--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
diff --git a/git-gui/lib/checkout_op.tcl b/git-gui/lib/checkout_op.tcl
index a5228297db..21ea768d80 100644
--- a/git-gui/lib/checkout_op.tcl
+++ b/git-gui/lib/checkout_op.tcl
@@ -341,9 +341,9 @@ method _readtree {} {
global HEAD
set readtree_d {}
- $::main_status start \
+ set status_bar_operation [$::main_status start \
[mc "Updating working directory to '%s'..." [_name $this]] \
- [mc "files checked out"]
+ [mc "files checked out"]]
set fd [git_read --stderr read-tree \
-m \
@@ -354,26 +354,27 @@ method _readtree {} {
$new_hash \
]
fconfigure $fd -blocking 0 -translation binary
- fileevent $fd readable [cb _readtree_wait $fd]
+ fileevent $fd readable [cb _readtree_wait $fd $status_bar_operation]
}
-method _readtree_wait {fd} {
+method _readtree_wait {fd status_bar_operation} {
global current_branch
set buf [read $fd]
- $::main_status update_meter $buf
+ $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
- $::main_status stop [mc "Aborted checkout of '%s' (file level merging is required)." [_name $this]]
+ $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
@@ -384,7 +385,7 @@ $err
return
}
- $::main_status stop
+ $status_bar_operation stop
_after_readtree $this
}
diff --git a/git-gui/lib/choose_repository.tcl b/git-gui/lib/choose_repository.tcl
index 80f5a59bbb..e54f3e66d8 100644
--- a/git-gui/lib/choose_repository.tcl
+++ b/git-gui/lib/choose_repository.tcl
@@ -9,6 +9,18 @@ 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
@@ -659,12 +671,12 @@ method _do_clone2 {} {
switch -exact -- $clone_type {
hardlink {
- set o_cons [status_bar::two_line $w_body]
+ set o_status [status_bar::two_line $w_body]
pack $w_body -fill x -padx 10 -pady 10
- $o_cons start \
+ set status_op [$o_status start \
[mc "Counting objects"] \
- [mc "buckets"]
+ [mc "buckets"]]
update
if {[file exists [file join $objdir info alternates]]} {
@@ -689,6 +701,7 @@ method _do_clone2 {} {
} err]} {
catch {cd $pwd}
_clone_failed $this [mc "Unable to copy objects/info/alternates: %s" $err]
+ $status_op stop
return
}
}
@@ -700,7 +713,7 @@ method _do_clone2 {} {
-directory [file join $objdir] ??]
set bcnt [expr {[llength $buckets] + 2}]
set bcur 1
- $o_cons update $bcur $bcnt
+ $status_op update $bcur $bcnt
update
file mkdir [file join .git objects pack]
@@ -708,7 +721,7 @@ method _do_clone2 {} {
-directory [file join $objdir pack] *] {
lappend tolink [file join pack $i]
}
- $o_cons update [incr bcur] $bcnt
+ $status_op update [incr bcur] $bcnt
update
foreach i $buckets {
@@ -717,10 +730,10 @@ method _do_clone2 {} {
-directory [file join $objdir $i] *] {
lappend tolink [file join $i $j]
}
- $o_cons update [incr bcur] $bcnt
+ $status_op update [incr bcur] $bcnt
update
}
- $o_cons stop
+ $status_op stop
if {$tolink eq {}} {
info_popup [strcat \
@@ -747,6 +760,8 @@ method _do_clone2 {} {
if {!$i} return
destroy $w_body
+
+ set o_status {}
}
full {
set o_cons [console::embed \
@@ -781,9 +796,9 @@ method _do_clone2 {} {
}
method _copy_files {objdir tocopy} {
- $o_cons start \
+ set status_op [$o_status start \
[mc "Copying objects"] \
- [mc "KiB"]
+ [mc "KiB"]]
set tot 0
set cmp 0
foreach p $tocopy {
@@ -798,7 +813,7 @@ method _copy_files {objdir tocopy} {
while {![eof $f_in]} {
incr cmp [fcopy $f_in $f_cp -size 16384]
- $o_cons update \
+ $status_op update \
[expr {$cmp / 1024}] \
[expr {$tot / 1024}]
update
@@ -808,17 +823,19 @@ method _copy_files {objdir tocopy} {
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]
- $o_cons start \
+ set status_op [$o_status start \
[mc "Linking objects"] \
- [mc "objects"]
+ [mc "objects"]]
for {set i 0} {$i < $total} {} {
set p [lindex $tolink $i]
if {[catch {
@@ -827,15 +844,17 @@ method _link_files {objdir tolink} {
[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} {
- $o_cons update $i $total
+ $status_op update $i $total
update
}
}
+ $status_op stop
return 1
}
@@ -958,11 +977,26 @@ method _do_clone_checkout {HEAD} {
return
}
- set o_cons [status_bar::two_line $w_body]
+ set status [status_bar::two_line $w_body]
pack $w_body -fill x -padx 10 -pady 10
- $o_cons start \
+
+ # 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"]
+ [mc "files"]]
set readtree_err {}
set fd [git_read --stderr read-tree \
@@ -976,33 +1010,9 @@ method _do_clone_checkout {HEAD} {
fileevent $fd readable [cb _readtree_wait $fd]
}
-method _do_validate_submodule_cloning {ok} {
- if {$ok} {
- $o_cons done $ok
- set done 1
- } else {
- _clone_failed $this [mc "Cannot clone submodules."]
- }
-}
-
-method _do_clone_submodules {} {
- if {$recursive eq {true}} {
- 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 _readtree_wait {fd} {
set buf [read $fd]
- $o_cons update_meter $buf
+ $o_status_op update_meter $buf
append readtree_err $buf
fconfigure $fd -blocking 1
@@ -1050,6 +1060,34 @@ method _postcheckout_wait {fd_ph} {
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
diff --git a/git-gui/lib/chord.tcl b/git-gui/lib/chord.tcl
new file mode 100644
index 0000000000..275a6cd4a1
--- /dev/null
+++ b/git-gui/lib/chord.tcl
@@ -0,0 +1,160 @@
+# 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]
+#
+# # Pass notes as 'after' callbacks to other operations
+# async_operation $args [$chord add_note]
+# other_async_operation $args [$chord add_note]
+#
+# # 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
+#
+# 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".
+oo::class create SimpleChord {
+ variable notes body is_completed
+
+ # 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 {body} {
+ set notes [list]
+ my eval [list set body $body]
+ set is_completed 0
+ }
+
+ # 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 [self] $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 [self]]
+
+ 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 [self] $body
+ namespace delete [self]
+ }
+ }
+}
+
+# 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.
+oo::class create ChordNote {
+ variable chord is_activated
+
+ # Constructor:
+ # Instances of ChordNote are created internally by calling add_note on
+ # SimpleChord objects.
+ constructor {chord} {
+ my eval set chord $chord
+ set is_activated 0
+ }
+
+ # Method:
+ # [$note is_activated]
+ # Returns true if this note has already been activated.
+ method is_activated {} {
+ return $is_activated
+ }
+
+ # Method:
+ # $note
+ # 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.
+ #
+ # NB: In TclOO, if an object is invoked like a method without supplying
+ # any method name, then this internal method `unknown` is what
+ # actually runs (with no parameters). It is used in the ChordNote
+ # class for the purpose of allowing the note object to be called as
+ # a function (see example above). (The `unknown` method can also be
+ # used to support dynamic dispatch, but must take parameters to
+ # identify the "unknown" method to be invoked. In this form, this
+ # proc serves only to make instances behave directly like methods.)
+ method unknown {} {
+ if {!$is_activated} {
+ set is_activated 1
+ $chord notify_note_activation
+ }
+ }
+}
diff --git a/git-gui/lib/console.tcl b/git-gui/lib/console.tcl
index 1f3248ffd1..bb6b9c889e 100644
--- a/git-gui/lib/console.tcl
+++ b/git-gui/lib/console.tcl
@@ -203,6 +203,8 @@ method done {ok} {
focus $w.ok
}
}
+
+ bind $w <Key-Escape> "destroy $w;break"
}
method _sb_set {sb orient first last} {
diff --git a/git-gui/lib/index.tcl b/git-gui/lib/index.tcl
index e07b7a3762..1254145634 100644
--- a/git-gui/lib/index.tcl
+++ b/git-gui/lib/index.tcl
@@ -7,67 +7,74 @@ proc _delete_indexlock {} {
}
}
-proc _close_updateindex {fd after} {
- global use_ttk NS
- fconfigure $fd -blocking 1
- if {[catch {close $fd} err]} {
- 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
+proc close_and_unlock_index {fd after} {
+ if {![catch {_close_updateindex $fd} err]} {
unlock_index
- rescan $after 0
- return
+ uplevel #0 $after
+ } else {
+ rescan_on_error $err $after
}
+}
- $::main_status stop
+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
- uplevel #0 $after
+ rescan [concat $after [list ui_ready]] 0
}
-proc update_indexinfo {msg pathList after} {
+proc update_indexinfo {msg path_list after} {
global update_index_cp
if {![lock_index update]} return
set update_index_cp 0
- set pathList [lsort $pathList]
- set totalCnt [llength $pathList]
- set batch [expr {int($totalCnt * .01) + 1}]
+ 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}
- $::main_status start $msg [mc "files"]
+ set status_bar_operation [$::main_status start $msg [mc "files"]]
set fd [git_write update-index -z --index-info]
fconfigure $fd \
-blocking 0 \
@@ -78,26 +85,29 @@ proc update_indexinfo {msg pathList after} {
fileevent $fd writable [list \
write_update_indexinfo \
$fd \
- $pathList \
- $totalCnt \
+ $path_list \
+ $total_cnt \
$batch \
+ $status_bar_operation \
$after \
]
}
-proc write_update_indexinfo {fd pathList totalCnt batch 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 >= $totalCnt} {
- _close_updateindex $fd $after
+ if {$update_index_cp >= $total_cnt} {
+ $status_bar_operation stop
+ close_and_unlock_index $fd $after
return
}
for {set i $batch} \
- {$update_index_cp < $totalCnt && $i > 0} \
+ {$update_index_cp < $total_cnt && $i > 0} \
{incr i -1} {
- set path [lindex $pathList $update_index_cp]
+ set path [lindex $path_list $update_index_cp]
incr update_index_cp
set s $file_states($path)
@@ -119,21 +129,21 @@ proc write_update_indexinfo {fd pathList totalCnt batch after} {
display_file $path $new
}
- $::main_status update $update_index_cp $totalCnt
+ $status_bar_operation update $update_index_cp $total_cnt
}
-proc update_index {msg pathList after} {
+proc update_index {msg path_list after} {
global update_index_cp
if {![lock_index update]} return
set update_index_cp 0
- set pathList [lsort $pathList]
- set totalCnt [llength $pathList]
- set batch [expr {int($totalCnt * .01) + 1}]
+ 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}
- $::main_status start $msg [mc "files"]
+ set status_bar_operation [$::main_status start $msg [mc "files"]]
set fd [git_write update-index --add --remove -z --stdin]
fconfigure $fd \
-blocking 0 \
@@ -144,26 +154,29 @@ proc update_index {msg pathList after} {
fileevent $fd writable [list \
write_update_index \
$fd \
- $pathList \
- $totalCnt \
+ $path_list \
+ $total_cnt \
$batch \
+ $status_bar_operation \
$after \
]
}
-proc write_update_index {fd pathList totalCnt batch 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 >= $totalCnt} {
- _close_updateindex $fd $after
+ if {$update_index_cp >= $total_cnt} {
+ $status_bar_operation stop
+ close_and_unlock_index $fd $after
return
}
for {set i $batch} \
- {$update_index_cp < $totalCnt && $i > 0} \
+ {$update_index_cp < $total_cnt && $i > 0} \
{incr i -1} {
- set path [lindex $pathList $update_index_cp]
+ set path [lindex $path_list $update_index_cp]
incr update_index_cp
switch -glob -- [lindex $file_states($path) 0] {
@@ -190,21 +203,21 @@ proc write_update_index {fd pathList totalCnt batch after} {
display_file $path $new
}
- $::main_status update $update_index_cp $totalCnt
+ $status_bar_operation update $update_index_cp $total_cnt
}
-proc checkout_index {msg pathList after} {
+proc checkout_index {msg path_list after capture_error} {
global update_index_cp
if {![lock_index update]} return
set update_index_cp 0
- set pathList [lsort $pathList]
- set totalCnt [llength $pathList]
- set batch [expr {int($totalCnt * .01) + 1}]
+ 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}
- $::main_status start $msg [mc "files"]
+ set status_bar_operation [$::main_status start $msg [mc "files"]]
set fd [git_write checkout-index \
--index \
--quiet \
@@ -221,26 +234,45 @@ proc checkout_index {msg pathList after} {
fileevent $fd writable [list \
write_checkout_index \
$fd \
- $pathList \
- $totalCnt \
+ $path_list \
+ $total_cnt \
$batch \
+ $status_bar_operation \
$after \
+ $capture_error \
]
}
-proc write_checkout_index {fd pathList totalCnt batch after} {
+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 >= $totalCnt} {
- _close_updateindex $fd $after
+ 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 < $totalCnt && $i > 0} \
+ {$update_index_cp < $total_cnt && $i > 0} \
{incr i -1} {
- set path [lindex $pathList $update_index_cp]
+ set path [lindex $path_list $update_index_cp]
incr update_index_cp
switch -glob -- [lindex $file_states($path) 0] {
U? {continue}
@@ -253,7 +285,7 @@ proc write_checkout_index {fd pathList totalCnt batch after} {
}
}
- $::main_status update $update_index_cp $totalCnt
+ $status_bar_operation update $update_index_cp $total_cnt
}
proc unstage_helper {txt paths} {
@@ -261,7 +293,7 @@ proc unstage_helper {txt paths} {
if {![lock_index begin-update]} return
- set pathList [list]
+ set path_list [list]
set after {}
foreach path $paths {
switch -glob -- [lindex $file_states($path) 0] {
@@ -269,19 +301,19 @@ proc unstage_helper {txt paths} {
M? -
T? -
D? {
- lappend pathList $path
+ lappend path_list $path
if {$path eq $current_diff_path} {
set after {reshow_diff;}
}
}
}
}
- if {$pathList eq {}} {
+ if {$path_list eq {}} {
unlock_index
} else {
update_indexinfo \
$txt \
- $pathList \
+ $path_list \
[concat $after [list ui_ready]]
}
}
@@ -305,7 +337,7 @@ proc add_helper {txt paths} {
if {![lock_index begin-update]} return
- set pathList [list]
+ set path_list [list]
set after {}
foreach path $paths {
switch -glob -- [lindex $file_states($path) 0] {
@@ -321,19 +353,19 @@ proc add_helper {txt paths} {
?M -
?D -
?T {
- lappend pathList $path
+ lappend path_list $path
if {$path eq $current_diff_path} {
set after {reshow_diff;}
}
}
}
}
- if {$pathList eq {}} {
+ if {$path_list eq {}} {
unlock_index
} else {
update_index \
$txt \
- $pathList \
+ $path_list \
[concat $after {ui_status [mc "Ready to commit."]}]
}
}
@@ -388,66 +420,301 @@ proc do_add_all {} {
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
- set pathList [list]
- set after {}
+ # 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 pathList $path
+ lappend path_list $path
if {$path eq $current_diff_path} {
- set after {reshow_diff;}
+ $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]
+ }
- # 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.
- #
- set n [llength $pathList]
- if {$n == 0} {
- unlock_index
- return
- } elseif {$n == 1} {
- set query [mc "Revert changes in file %s?" [short_path [lindex $pathList]]]
- } else {
- set query [mc "Revert changes in these %i files?" $n]
+ 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} {
+ checkout_index \
+ $txt \
+ $path_list \
+ [$after_chord add_note] \
+ $capture_error
+ }
}
- set reply [tk_dialog \
- .confirm_revert \
- "[appname] ([reponame])" \
- "$query
+ # 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 \
+ ]
+ }
-[mc "Any unstaged changes will be permanently lost by the revert."]" \
- question \
- 1 \
- [mc "Do Nothing"] \
- [mc "Revert Changes"] \
- ]
- if {$reply == 1} {
- checkout_index \
- $txt \
- $pathList \
- [concat $after [list ui_ready]]
+ 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 }
+
+ delete_files $untracked_list [$after_chord add_note]
+ }
+ }
+
+ # 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
+}
+
+# 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 {
- unlock_index
+ # 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]
}
}
diff --git a/git-gui/lib/merge.tcl b/git-gui/lib/merge.tcl
index 9f253db5b3..8df8ffae55 100644
--- a/git-gui/lib/merge.tcl
+++ b/git-gui/lib/merge.tcl
@@ -241,23 +241,27 @@ 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
- fileevent $fd readable [namespace code [list _reset_wait $fd]]
- $::main_status start [mc "Aborting"] [mc "files reset"]
+ 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} {
+proc _reset_wait {fd status_bar_operation} {
global ui_comm
- $::main_status update_meter [read $fd]
+ $status_bar_operation update_meter [read $fd]
fconfigure $fd -blocking 1
if {[eof $fd]} {
set fail [catch {close $fd} err]
- $::main_status stop
unlock_index
+ $status_bar_operation stop
$ui_comm delete 0.0 end
$ui_comm edit modified false
diff --git a/git-gui/lib/status_bar.tcl b/git-gui/lib/status_bar.tcl
index 02111a1742..d32b14142f 100644
--- a/git-gui/lib/status_bar.tcl
+++ b/git-gui/lib/status_bar.tcl
@@ -1,16 +1,42 @@
# 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 status {}; # single line of text we show
-field prefix {}; # text we format into status
-field units {}; # unit of progress
-field meter {}; # current core git progress meter (if active)
+
+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
@@ -18,12 +44,19 @@ constructor new {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 \
+ -textvariable @status_bar_text \
-anchor w \
-justify left
pack $w_l -side left
@@ -44,9 +77,16 @@ constructor two_line {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 \
+ -textvariable @status_bar_text \
-anchor w \
-justify left
pack $w_l -anchor w -fill x
@@ -56,7 +96,7 @@ constructor two_line {path} {
return $this
}
-method start {msg uds} {
+method ensure_canvas {} {
if {[winfo exists $w_c]} {
$w_c coords bar 0 0 0 20
} else {
@@ -68,31 +108,170 @@ method start {msg uds} {
$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} {
- set pdone 0
- set cdone 0
+ if {!$is_active} { return }
+
+ set progress 0
+
if {$total > 0} {
- set pdone [expr {100 * $have / $total}]
- set cdone [expr {[winfo width $w_c] * $have / $total}]
+ 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 $pdone]
- $w_c coords bar 0 0 $cdone 20
+ $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} {
@@ -109,23 +288,25 @@ method update_meter {buf} {
}
}
-method stop {{msg {}}} {
- destroy $w_c
- if {$msg ne {}} {
- set status $msg
+method stop {{stop_msg {}}} {
+ if {$is_active} {
+ set is_active 0
+ $status_bar stop $this $stop_msg
}
}
-method show {msg {test {}}} {
- if {$test eq {} || $status eq $test} {
- set status $msg
- }
+method restart {msg} {
+ if {!$is_active} { return }
+
+ set status $msg
+ set prefix $msg
+ set meter {}
+ $status_bar refresh
}
-method _delete {current} {
- if {$current eq $w} {
- delete_this
- }
+method _delete {} {
+ stop
+ delete_this
}
}
diff --git a/git-p4.py b/git-p4.py
index 0b3a07cb31..40d9e7c594 100755
--- a/git-p4.py
+++ b/git-p4.py
@@ -167,6 +167,21 @@ def die(msg):
sys.stderr.write(msg + "\n")
sys.exit(1)
+def prompt(prompt_text):
+ """ Prompt the user to choose one of the choices
+
+ Choices are identified in the prompt_text by square brackets around
+ a single letter option.
+ """
+ choices = set(m.group(1) for m in re.finditer(r"\[(.)\]", prompt_text))
+ while True:
+ response = raw_input(prompt_text).strip().lower()
+ if not response:
+ continue
+ response = response[0]
+ if response in choices:
+ return response
+
def write_pipe(c, stdin):
if verbose:
sys.stderr.write('Writing pipe: %s\n' % str(c))
@@ -1784,12 +1799,11 @@ class P4Submit(Command, P4UserMap):
if os.stat(template_file).st_mtime > mtime:
return True
- while True:
- response = raw_input("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
- if response == 'y':
- return True
- if response == 'n':
- return False
+ response = prompt("Submit template unchanged. Submit anyway? [y]es, [n]o (skip this patch) ")
+ if response == 'y':
+ return True
+ if response == 'n':
+ return False
def get_diff_description(self, editedFiles, filesToAdd, symlinks):
# diff
@@ -2351,31 +2365,22 @@ class P4Submit(Command, P4UserMap):
" --prepare-p4-only")
break
if i < last:
- quit = False
- while True:
- # prompt for what to do, or use the option/variable
- if self.conflict_behavior == "ask":
- print("What do you want to do?")
- response = raw_input("[s]kip this commit but apply"
- " the rest, or [q]uit? ")
- if not response:
- continue
- elif self.conflict_behavior == "skip":
- response = "s"
- elif self.conflict_behavior == "quit":
- response = "q"
- else:
- die("Unknown conflict_behavior '%s'" %
- self.conflict_behavior)
-
- if response[0] == "s":
- print("Skipping this commit, but applying the rest")
- break
- if response[0] == "q":
- print("Quitting")
- quit = True
- break
- if quit:
+ # prompt for what to do, or use the option/variable
+ if self.conflict_behavior == "ask":
+ print("What do you want to do?")
+ response = prompt("[s]kip this commit but apply the rest, or [q]uit? ")
+ elif self.conflict_behavior == "skip":
+ response = "s"
+ elif self.conflict_behavior == "quit":
+ response = "q"
+ else:
+ die("Unknown conflict_behavior '%s'" %
+ self.conflict_behavior)
+
+ if response == "s":
+ print("Skipping this commit, but applying the rest")
+ if response == "q":
+ print("Quitting")
break
chdir(self.oldWorkingDirectory)
@@ -4146,7 +4151,12 @@ def main():
description = cmd.description,
formatter = HelpFormatter())
- (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
+ try:
+ (cmd, args) = parser.parse_args(sys.argv[2:], cmd);
+ except:
+ parser.print_help()
+ raise
+
global verbose
verbose = cmd.verbose
if cmd.needsGit:
diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index 0f857d790b..65a3a9e62e 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -741,7 +741,7 @@ sub evaluate_gitweb_config {
$GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
# Common system-wide settings for convenience.
- # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
+ # Those settings can be overridden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
read_config_file($GITWEB_CONFIG_COMMON);
# Use first config file that exists. This means use the per-instance
@@ -5285,7 +5285,7 @@ sub format_ctx_rem_add_lines {
# + c
# + d
#
- # Otherwise the highlightling would be confusing.
+ # Otherwise the highlighting would be confusing.
if ($is_combined) {
for (my $i = 0; $i < @$add; $i++) {
my $prefix_rem = substr($rem->[$i], 0, $num_parents);
diff --git a/graph.c b/graph.c
index 66ae18add8..aaf97069bd 100644
--- a/graph.c
+++ b/graph.c
@@ -1063,7 +1063,7 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
int i, j;
struct commit_list *first_parent = first_interesting_parent(graph);
- int seen_parent = 0;
+ struct column *parent_col = NULL;
/*
* Output the post-merge row
@@ -1117,12 +1117,17 @@ static void graph_output_post_merge_line(struct git_graph *graph, struct graph_l
graph_line_addch(line, ' ');
} else {
graph_line_write_column(line, col, '|');
- if (graph->merge_layout != 0 || i != graph->commit_index - 1)
- graph_line_addch(line, seen_parent ? '_' : ' ');
+ if (graph->merge_layout != 0 || i != graph->commit_index - 1) {
+ if (parent_col)
+ graph_line_write_column(
+ line, parent_col, '_');
+ else
+ graph_line_addch(line, ' ');
+ }
}
if (col_commit == first_parent->item)
- seen_parent = 1;
+ parent_col = col;
}
/*
@@ -1219,13 +1224,9 @@ static void graph_output_collapsing_line(struct git_graph *graph, struct graph_l
*
* The space just to the left of this
* branch should always be empty.
- *
- * The branch to the left of that space
- * should be our eventual target.
*/
assert(graph->mapping[i - 1] > target);
assert(graph->mapping[i - 2] < 0);
- assert(graph->mapping[i - 3] == target);
graph->mapping[i - 2] = target;
/*
* Mark this branch as the horizontal edge to
diff --git a/merge-recursive.c b/merge-recursive.c
index 11869ad81c..10dca5644b 100644
--- a/merge-recursive.c
+++ b/merge-recursive.c
@@ -224,17 +224,6 @@ static struct commit *make_virtual_commit(struct repository *repo,
return commit;
}
-/*
- * Since we use get_tree_entry(), which does not put the read object into
- * the object pool, we cannot rely on a == b.
- */
-static int oid_eq(const struct object_id *a, const struct object_id *b)
-{
- if (!a && !b)
- return 2;
- return a && b && oideq(a, b);
-}
-
enum rename_type {
RENAME_NORMAL = 0,
RENAME_VIA_DIR,
@@ -805,7 +794,7 @@ static int was_tracked_and_matches(struct merge_options *opt, const char *path,
/* See if the file we were tracking before matches */
ce = opt->priv->orig_index.cache[pos];
- return (oid_eq(&ce->oid, &blob->oid) && ce->ce_mode == blob->mode);
+ return (oideq(&ce->oid, &blob->oid) && ce->ce_mode == blob->mode);
}
/*
@@ -1317,7 +1306,7 @@ static int merge_mode_and_contents(struct merge_options *opt,
oidcpy(&result->blob.oid, &b->oid);
}
} else {
- if (!oid_eq(&a->oid, &o->oid) && !oid_eq(&b->oid, &o->oid))
+ if (!oideq(&a->oid, &o->oid) && !oideq(&b->oid, &o->oid))
result->merge = 1;
/*
@@ -1333,9 +1322,9 @@ static int merge_mode_and_contents(struct merge_options *opt,
}
}
- if (oid_eq(&a->oid, &b->oid) || oid_eq(&a->oid, &o->oid))
+ if (oideq(&a->oid, &b->oid) || oideq(&a->oid, &o->oid))
oidcpy(&result->blob.oid, &b->oid);
- else if (oid_eq(&b->oid, &o->oid))
+ else if (oideq(&b->oid, &o->oid))
oidcpy(&result->blob.oid, &a->oid);
else if (S_ISREG(a->mode)) {
mmbuffer_t result_buf;
@@ -1368,7 +1357,7 @@ static int merge_mode_and_contents(struct merge_options *opt,
switch (opt->recursive_variant) {
case MERGE_VARIANT_NORMAL:
oidcpy(&result->blob.oid, &a->oid);
- if (!oid_eq(&a->oid, &b->oid))
+ if (!oideq(&a->oid, &b->oid))
result->clean = 0;
break;
case MERGE_VARIANT_OURS:
@@ -2836,15 +2825,15 @@ static int process_renames(struct merge_options *opt,
dst_other.mode = ren1->dst_entry->stages[other_stage].mode;
try_merge = 0;
- if (oid_eq(&src_other.oid, &null_oid) &&
+ if (oideq(&src_other.oid, &null_oid) &&
ren1->dir_rename_original_type == 'A') {
setup_rename_conflict_info(RENAME_VIA_DIR,
opt, ren1, NULL);
- } else if (oid_eq(&src_other.oid, &null_oid)) {
+ } else if (oideq(&src_other.oid, &null_oid)) {
setup_rename_conflict_info(RENAME_DELETE,
opt, ren1, NULL);
} else if ((dst_other.mode == ren1->pair->two->mode) &&
- oid_eq(&dst_other.oid, &ren1->pair->two->oid)) {
+ oideq(&dst_other.oid, &ren1->pair->two->oid)) {
/*
* Added file on the other side identical to
* the file being renamed: clean merge.
@@ -2859,7 +2848,7 @@ static int process_renames(struct merge_options *opt,
1, /* update_cache */
0 /* update_wd */))
clean_merge = -1;
- } else if (!oid_eq(&dst_other.oid, &null_oid)) {
+ } else if (!oideq(&dst_other.oid, &null_oid)) {
/*
* Probably not a clean merge, but it's
* premature to set clean_merge to 0 here,
@@ -3037,7 +3026,7 @@ static int blob_unchanged(struct merge_options *opt,
if (a->mode != o->mode)
return 0;
- if (oid_eq(&o->oid, &a->oid))
+ if (oideq(&o->oid, &a->oid))
return 1;
if (!renormalize)
return 0;
@@ -3478,7 +3467,7 @@ static int merge_trees_internal(struct merge_options *opt,
opt->subtree_shift);
}
- if (oid_eq(&merge_base->object.oid, &merge->object.oid)) {
+ if (oideq(&merge_base->object.oid, &merge->object.oid)) {
output(opt, 0, _("Already up to date!"));
*result = head;
return 1;
diff --git a/packfile.c b/packfile.c
index f0dc63e92f..7e7c04e4d8 100644
--- a/packfile.c
+++ b/packfile.c
@@ -510,7 +510,6 @@ static int open_packed_git_1(struct packed_git *p)
struct pack_header hdr;
unsigned char hash[GIT_MAX_RAWSZ];
unsigned char *idx_hash;
- long fd_flag;
ssize_t read_result;
const unsigned hashsz = the_hash_algo->rawsz;
@@ -554,16 +553,6 @@ static int open_packed_git_1(struct packed_git *p)
} else if (p->pack_size != st.st_size)
return error("packfile %s size changed", p->pack_name);
- /* We leave these file descriptors open with sliding mmap;
- * there is no point keeping them open across exec(), though.
- */
- fd_flag = fcntl(p->pack_fd, F_GETFD, 0);
- if (fd_flag < 0)
- return error("cannot determine file descriptor flags");
- fd_flag |= FD_CLOEXEC;
- if (fcntl(p->pack_fd, F_SETFD, fd_flag) == -1)
- return error("cannot set FD_CLOEXEC");
-
/* Verify we recognize this pack file format. */
read_result = read_in_full(p->pack_fd, &hdr, sizeof(hdr));
if (read_result < 0)
@@ -587,9 +576,8 @@ static int open_packed_git_1(struct packed_git *p)
" while index indicates %"PRIu32" objects",
p->pack_name, ntohl(hdr.hdr_entries),
p->num_objects);
- if (lseek(p->pack_fd, p->pack_size - hashsz, SEEK_SET) == -1)
- return error("end of packfile %s is unavailable", p->pack_name);
- read_result = read_in_full(p->pack_fd, hash, hashsz);
+ read_result = pread_in_full(p->pack_fd, hash, hashsz,
+ p->pack_size - hashsz);
if (read_result < 0)
return error_errno("error reading from %s", p->pack_name);
if (read_result != hashsz)
diff --git a/read-cache.c b/read-cache.c
index ad0b48c84d..aa427c5c17 100644
--- a/read-cache.c
+++ b/read-cache.c
@@ -959,7 +959,7 @@ static int verify_dotfile(const char *rest, unsigned mode)
int verify_path(const char *path, unsigned mode)
{
- char c;
+ char c = 0;
if (has_dos_drive_prefix(path))
return 0;
@@ -974,6 +974,7 @@ int verify_path(const char *path, unsigned mode)
if (is_dir_sep(c)) {
inside:
if (protect_hfs) {
+
if (is_hfs_dotgit(path))
return 0;
if (S_ISLNK(mode)) {
@@ -982,6 +983,10 @@ inside:
}
}
if (protect_ntfs) {
+#ifdef GIT_WINDOWS_NATIVE
+ if (c == '\\')
+ return 0;
+#endif
if (is_ntfs_dotgit(path))
return 0;
if (S_ISLNK(mode)) {
diff --git a/sequencer.c b/sequencer.c
index 763ccbbc45..b9dbf1adb0 100644
--- a/sequencer.c
+++ b/sequencer.c
@@ -147,8 +147,6 @@ static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
* command-line.
*/
static GIT_PATH_FUNC(rebase_path_gpg_sign_opt, "rebase-merge/gpg_sign_opt")
-static GIT_PATH_FUNC(rebase_path_cdate_is_adate, "rebase-merge/cdate_is_adate")
-static GIT_PATH_FUNC(rebase_path_ignore_date, "rebase-merge/ignore_date")
static GIT_PATH_FUNC(rebase_path_orig_head, "rebase-merge/orig-head")
static GIT_PATH_FUNC(rebase_path_verbose, "rebase-merge/verbose")
static GIT_PATH_FUNC(rebase_path_quiet, "rebase-merge/quiet")
@@ -825,19 +823,9 @@ int read_author_script(const char *path, char **name, char **email, char **date,
error(_("missing 'GIT_AUTHOR_DATE'"));
if (date_i < 0 || email_i < 0 || date_i < 0 || err)
goto finish;
-
- if (name)
- *name = kv.items[name_i].util;
- else
- free(kv.items[name_i].util);
- if (email)
- *email = kv.items[email_i].util;
- else
- free(kv.items[email_i].util);
- if (date)
- *date = kv.items[date_i].util;
- else
- free(kv.items[date_i].util);
+ *name = kv.items[name_i].util;
+ *email = kv.items[email_i].util;
+ *date = kv.items[date_i].util;
retval = 0;
finish:
string_list_clear(&kv, !!retval);
@@ -880,47 +868,6 @@ static char *get_author(const char *message)
return NULL;
}
-/* Returns a "date" string that needs to be free()'d by the caller */
-static char *read_author_date_or_null(void)
-{
- char *date;
-
- if (read_author_script(rebase_path_author_script(),
- NULL, NULL, &date, 0))
- return NULL;
- return date;
-}
-
-/* Construct a free()able author string with current time as the author date */
-static char *ignore_author_date(const char *author)
-{
- int len = strlen(author);
- struct ident_split ident;
- struct strbuf new_author = STRBUF_INIT;
-
- if (split_ident_line(&ident, author, len) < 0) {
- error(_("malformed ident line"));
- return NULL;
- }
- len = ident.mail_end - ident.name_begin + 1;
-
- strbuf_addf(&new_author, "%.*s ", len, ident.name_begin);
- datestamp(&new_author);
- return strbuf_detach(&new_author, NULL);
-}
-
-static void push_dates(struct child_process *child, int change_committer_date)
-{
- time_t now = time(NULL);
- struct strbuf date = STRBUF_INIT;
-
- strbuf_addf(&date, "@%"PRIuMAX, (uintmax_t)now);
- argv_array_pushf(&child->env_array, "GIT_AUTHOR_DATE=%s", date.buf);
- if (change_committer_date)
- argv_array_pushf(&child->env_array, "GIT_COMMITTER_DATE=%s", date.buf);
- strbuf_release(&date);
-}
-
static const char staged_changes_advice[] =
N_("you have staged changes in your working tree\n"
"If these changes are meant to be squashed into the previous commit, run:\n"
@@ -980,25 +927,6 @@ static int run_git_commit(struct repository *r,
cmd.git_cmd = 1;
- if (opts->committer_date_is_author_date) {
- int res = -1;
- struct strbuf datebuf = STRBUF_INIT;
- char *date = read_author_date_or_null();
-
- if (!date)
- return -1;
-
- strbuf_addf(&datebuf, "@%s", date);
- res = setenv("GIT_COMMITTER_DATE",
- opts->ignore_date ? "" : datebuf.buf, 1);
-
- strbuf_release(&datebuf);
- free(date);
-
- if (res)
- return -1;
- }
-
if (is_rebase_i(opts) && read_env_script(&cmd.env_array)) {
const char *gpg_opt = gpg_sign_opt_quoted(opts);
@@ -1014,8 +942,6 @@ static int run_git_commit(struct repository *r,
argv_array_push(&cmd.args, "--amend");
if (opts->gpg_sign)
argv_array_pushf(&cmd.args, "-S%s", opts->gpg_sign);
- if (opts->ignore_date)
- push_dates(&cmd, opts->committer_date_is_author_date);
if (defmsg)
argv_array_pushl(&cmd.args, "-F", defmsg, NULL);
else if (!(flags & EDIT_MSG))
@@ -1384,13 +1310,14 @@ static int try_to_commit(struct repository *r,
struct commit_extra_header *extra = NULL;
struct strbuf err = STRBUF_INIT;
struct strbuf commit_msg = STRBUF_INIT;
- char *author_to_free = NULL;
+ char *amend_author = NULL;
const char *hook_commit = NULL;
enum commit_msg_cleanup_mode cleanup;
int res = 0;
if (parse_head(r, &current_head))
return -1;
+
if (flags & AMEND_MSG) {
const char *exclude_gpgsig[] = { "gpgsig", NULL };
const char *out_enc = get_commit_output_encoding();
@@ -1405,7 +1332,7 @@ static int try_to_commit(struct repository *r,
strbuf_addstr(msg, orig_message);
hook_commit = "HEAD";
}
- author = author_to_free = get_author(message);
+ author = amend_author = get_author(message);
unuse_commit_buffer(current_head, message);
if (!author) {
res = error(_("unable to parse commit author"));
@@ -1418,31 +1345,6 @@ static int try_to_commit(struct repository *r,
commit_list_insert(current_head, &parents);
}
- if (opts->committer_date_is_author_date) {
- int len = strlen(author);
- struct ident_split ident;
- struct strbuf date = STRBUF_INIT;
-
- if (split_ident_line(&ident, author, len) < 0) {
- res = error(_("malformed ident line"));
- goto out;
- }
- if (!ident.date_begin) {
- res = error(_("corrupted author without date information"));
- goto out;
- }
-
- strbuf_addf(&date, "@%.*s %.*s",
- (int)(ident.date_end - ident.date_begin), ident.date_begin,
- (int)(ident.tz_end - ident.tz_begin), ident.tz_begin);
- res = setenv("GIT_COMMITTER_DATE",
- opts->ignore_date ? "" : date.buf, 1);
- strbuf_release(&date);
-
- if (res)
- goto out;
- }
-
if (write_index_as_tree(&tree, r->index, r->index_file, 0, NULL)) {
res = error(_("git write-tree failed to write a tree"));
goto out;
@@ -1502,15 +1404,6 @@ static int try_to_commit(struct repository *r,
reset_ident_date();
- if (opts->ignore_date) {
- author = ignore_author_date(author);
- if (!author) {
- res = -1;
- goto out;
- }
- free(author_to_free);
- author_to_free = (char *)author;
- }
if (commit_tree_extended(msg->buf, msg->len, &tree, parents,
oid, author, opts->gpg_sign, extra)) {
res = error(_("failed to write commit object"));
@@ -1531,7 +1424,7 @@ out:
free_commit_extra_headers(extra);
strbuf_release(&err);
strbuf_release(&commit_msg);
- free(author_to_free);
+ free(amend_author);
return res;
}
@@ -2599,16 +2492,6 @@ static int read_populate_opts(struct replay_opts *opts)
opts->signoff = 1;
}
- if (file_exists(rebase_path_cdate_is_adate())) {
- opts->allow_ff = 0;
- opts->committer_date_is_author_date = 1;
- }
-
- if (file_exists(rebase_path_ignore_date())) {
- opts->allow_ff = 0;
- opts->ignore_date = 1;
- }
-
if (file_exists(rebase_path_reschedule_failed_exec()))
opts->reschedule_failed_exec = 1;
@@ -2691,10 +2574,6 @@ int write_basic_state(struct replay_opts *opts, const char *head_name,
write_file(rebase_path_gpg_sign_opt(), "-S%s\n", opts->gpg_sign);
if (opts->signoff)
write_file(rebase_path_signoff(), "--signoff\n");
- if (opts->committer_date_is_author_date)
- write_file(rebase_path_cdate_is_adate(), "%s", "");
- if (opts->ignore_date)
- write_file(rebase_path_ignore_date(), "%s", "");
if (opts->reschedule_failed_exec)
write_file(rebase_path_reschedule_failed_exec(), "%s", "");
@@ -3635,8 +3514,6 @@ static int do_merge(struct repository *r,
argv_array_push(&cmd.args, git_path_merge_msg(r));
if (opts->gpg_sign)
argv_array_push(&cmd.args, opts->gpg_sign);
- if (opts->ignore_date)
- push_dates(&cmd, opts->committer_date_is_author_date);
/* Add the tips to be merged */
for (j = to_merge; j; j = j->next)
@@ -3909,9 +3786,7 @@ static int pick_commits(struct repository *r,
setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
if (opts->allow_ff)
assert(!(opts->signoff || opts->no_commit ||
- opts->record_origin || opts->edit ||
- opts->committer_date_is_author_date ||
- opts->ignore_date));
+ opts->record_origin || opts->edit));
if (read_and_refresh_cache(r, opts))
return -1;
diff --git a/sequencer.h b/sequencer.h
index e9a0e03ea2..9f9ae291e3 100644
--- a/sequencer.h
+++ b/sequencer.h
@@ -43,8 +43,6 @@ struct replay_opts {
int verbose;
int quiet;
int reschedule_failed_exec;
- int committer_date_is_author_date;
- int ignore_date;
int mainline;
diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh
index de2df573a7..2ea2d00c39 100755
--- a/t/t0060-path-utils.sh
+++ b/t/t0060-path-utils.sh
@@ -469,11 +469,14 @@ test_expect_success 'match .gitmodules' '
'
test_expect_success MINGW 'is_valid_path() on Windows' '
- test-tool path-utils is_valid_path \
+ test-tool path-utils is_valid_path \
win32 \
"win32 x" \
../hello.txt \
C:\\git \
+ comm \
+ conout.c \
+ lptN \
\
--not \
"win32 " \
@@ -481,7 +484,13 @@ test_expect_success MINGW 'is_valid_path() on Windows' '
"win32." \
"win32 . ." \
.../hello.txt \
- colon:test
+ colon:test \
+ "AUX.c" \
+ "abc/conOut\$ .xyz/test" \
+ lpt8 \
+ "lpt*" \
+ Nul \
+ "PRN./abc"
'
test_done
diff --git a/t/t1091-sparse-checkout-builtin.sh b/t/t1091-sparse-checkout-builtin.sh
index 6f7e2d0c9e..ff7f8f7a1f 100755
--- a/t/t1091-sparse-checkout-builtin.sh
+++ b/t/t1091-sparse-checkout-builtin.sh
@@ -246,6 +246,17 @@ test_expect_success 'cone mode: init and set' '
test_cmp expect dir
'
+test_expect_success 'cone mode: list' '
+ cat >expect <<-EOF &&
+ folder1
+ folder2
+ EOF
+ git -C repo sparse-checkout set --stdin <expect &&
+ git -C repo sparse-checkout list >actual 2>err &&
+ test_must_be_empty err &&
+ test_cmp expect actual
+'
+
test_expect_success 'cone mode: set with nested folders' '
git -C repo sparse-checkout set deep deep/deeper1/deepest 2>err &&
test_line_count = 0 err &&
@@ -329,4 +340,32 @@ test_expect_success 'cone mode: set with core.ignoreCase=true' '
test_cmp expect dir
'
+test_expect_success 'interaction with submodules' '
+ git clone repo super &&
+ (
+ cd super &&
+ mkdir modules &&
+ git submodule add ../repo modules/child &&
+ git add . &&
+ git commit -m "add submodule" &&
+ git sparse-checkout init --cone &&
+ git sparse-checkout set folder1
+ ) &&
+ list_files super >dir &&
+ cat >expect <<-\EOF &&
+ a
+ folder1
+ modules
+ EOF
+ test_cmp expect dir &&
+ list_files super/modules/child >dir &&
+ cat >expect <<-\EOF &&
+ a
+ deep
+ folder1
+ folder2
+ EOF
+ test_cmp expect dir
+'
+
test_done
diff --git a/t/t3008-ls-files-lazy-init-name-hash.sh b/t/t3008-ls-files-lazy-init-name-hash.sh
index 64f047332b..85f3704958 100755
--- a/t/t3008-ls-files-lazy-init-name-hash.sh
+++ b/t/t3008-ls-files-lazy-init-name-hash.sh
@@ -4,7 +4,7 @@ test_description='Test the lazy init name hash with various folder structures'
. ./test-lib.sh
-if test 1 -eq $($GIT_BUILD_DIR/t/helper/test-tool online-cpus)
+if test 1 -eq $(test-tool online-cpus)
then
skip_all='skipping lazy-init tests, single cpu'
test_done
diff --git a/t/t3422-rebase-incompatible-options.sh b/t/t3422-rebase-incompatible-options.sh
index c8234062c6..50e7960702 100755
--- a/t/t3422-rebase-incompatible-options.sh
+++ b/t/t3422-rebase-incompatible-options.sh
@@ -61,6 +61,8 @@ test_rebase_am_only () {
}
test_rebase_am_only --whitespace=fix
+test_rebase_am_only --ignore-whitespace
+test_rebase_am_only --committer-date-is-author-date
test_rebase_am_only -C4
test_expect_success REBASE_P '--preserve-merges incompatible with --signoff' '
diff --git a/t/t3433-rebase-options-compatibility.sh b/t/t3433-rebase-options-compatibility.sh
deleted file mode 100755
index 5166f158dd..0000000000
--- a/t/t3433-rebase-options-compatibility.sh
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/bin/sh
-#
-# Copyright (c) 2019 Rohit Ashiwal
-#
-
-test_description='tests to ensure compatibility between am and interactive backends'
-
-. ./test-lib.sh
-
-GIT_AUTHOR_DATE="1999-04-02T08:03:20+05:30"
-export GIT_AUTHOR_DATE
-
-# This is a special case in which both am and interactive backends
-# provide the same output. It was done intentionally because
-# both the backends fall short of optimal behaviour.
-test_expect_success 'setup' '
- git checkout -b topic &&
- q_to_tab >file <<-\EOF &&
- line 1
- Qline 2
- line 3
- EOF
- git add file &&
- git commit -m "add file" &&
- cat >file <<-\EOF &&
- line 1
- new line 2
- line 3
- EOF
- git commit -am "update file" &&
- git tag side &&
- test_commit commit1 foo foo1 &&
- test_commit commit2 foo foo2 &&
- test_commit commit3 foo foo3 &&
-
- git checkout --orphan master &&
- git rm --cached foo &&
- rm foo &&
- sed -e "s/^|//" >file <<-\EOF &&
- |line 1
- | line 2
- |line 3
- EOF
- git add file &&
- git commit -m "add file" &&
- git tag main
-'
-
-test_expect_success '--ignore-whitespace works with am backend' '
- cat >expect <<-\EOF &&
- line 1
- new line 2
- line 3
- EOF
- test_must_fail git rebase main side &&
- git rebase --abort &&
- git rebase --ignore-whitespace main side &&
- test_cmp expect file
-'
-
-test_expect_success '--ignore-whitespace works with interactive backend' '
- cat >expect <<-\EOF &&
- line 1
- new line 2
- line 3
- EOF
- test_must_fail git rebase --merge main side &&
- git rebase --abort &&
- git rebase --merge --ignore-whitespace main side &&
- test_cmp expect file
-'
-
-test_expect_success '--committer-date-is-author-date works with am backend' '
- git commit --amend &&
- git rebase --committer-date-is-author-date HEAD^ &&
- git show HEAD --pretty="format:%ai" >authortime &&
- git show HEAD --pretty="format:%ci" >committertime &&
- test_cmp authortime committertime
-'
-
-test_expect_success '--committer-date-is-author-date works with interactive backend' '
- git commit --amend &&
- git rebase -i --committer-date-is-author-date HEAD^ &&
- git show HEAD --pretty="format:%ai" >authortime &&
- git show HEAD --pretty="format:%ci" >committertime &&
- test_cmp authortime committertime
-'
-
-test_expect_success '--committer-date-is-author-date works with rebase -r' '
- git checkout side &&
- git merge --no-ff commit3 &&
- git rebase -r --root --committer-date-is-author-date &&
- git rev-list HEAD >rev_list &&
- while read HASH
- do
- git show $HASH --pretty="format:%ai" >authortime
- git show $HASH --pretty="format:%ci" >committertime
- test_cmp authortime committertime
- done <rev_list
-'
-
-# Checking for +0000 in author time is enough since default
-# timezone is UTC, but the timezone used while committing
-# sets to +0530.
-test_expect_success '--ignore-date works with am backend' '
- git commit --amend --date="$GIT_AUTHOR_DATE" &&
- git rebase --ignore-date HEAD^ &&
- git show HEAD --pretty="format:%ai" >authortime &&
- grep "+0000" authortime
-'
-
-test_expect_success '--ignore-date works with interactive backend' '
- git commit --amend --date="$GIT_AUTHOR_DATE" &&
- git rebase --ignore-date -i HEAD^ &&
- git show HEAD --pretty="format:%ai" >authortime &&
- grep "+0000" authortime
-'
-
-test_expect_success '--ignore-date works with rebase -r' '
- git checkout side &&
- git merge --no-ff commit3 &&
- git rebase -r --root --ignore-date &&
- git rev-list HEAD >rev_list &&
- while read HASH
- do
- git show $HASH --pretty="format:%ai" >authortime
- grep "+0000" authortime
- done <rev_list
-'
-
-test_done
diff --git a/t/t4215-log-skewed-merges.sh b/t/t4215-log-skewed-merges.sh
index 18709a723e..5661ed5881 100755
--- a/t/t4215-log-skewed-merges.sh
+++ b/t/t4215-log-skewed-merges.sh
@@ -240,4 +240,75 @@ test_expect_success 'log --graph with octopus merge with column joining its penu
EOF
'
+test_expect_success 'log --graph with multiple tips' '
+ git checkout --orphan 6_1 &&
+ test_commit 6_A &&
+ git branch 6_2 &&
+ git branch 6_4 &&
+ test_commit 6_B &&
+ git branch 6_3 &&
+ test_commit 6_C &&
+ git checkout 6_2 && test_commit 6_D &&
+ git checkout 6_3 && test_commit 6_E &&
+ git checkout -b 6_5 6_1 &&
+ git merge --no-ff 6_2 -m 6_F &&
+ git checkout 6_4 && test_commit 6_G &&
+ git checkout 6_3 &&
+ git merge --no-ff 6_4 -m 6_H &&
+ git checkout 6_1 &&
+ git merge --no-ff 6_2 -m 6_I &&
+
+ check_graph 6_1 6_3 6_5 <<-\EOF
+ * 6_I
+ |\
+ | | * 6_H
+ | | |\
+ | | | * 6_G
+ | | * | 6_E
+ | | | | * 6_F
+ | |_|_|/|
+ |/| | |/
+ | | |/|
+ | |/| |
+ | * | | 6_D
+ | | |/
+ | |/|
+ * | | 6_C
+ | |/
+ |/|
+ * | 6_B
+ |/
+ * 6_A
+ EOF
+'
+
+test_expect_success 'log --graph with multiple tips and colors' '
+ test_config log.graphColors red,green,yellow,blue,magenta,cyan &&
+ cat >expect.colors <<-\EOF &&
+ * 6_I
+ <RED>|<RESET><GREEN>\<RESET>
+ <RED>|<RESET> <GREEN>|<RESET> * 6_H
+ <RED>|<RESET> <GREEN>|<RESET> <YELLOW>|<RESET><BLUE>\<RESET>
+ <RED>|<RESET> <GREEN>|<RESET> <YELLOW>|<RESET> * 6_G
+ <RED>|<RESET> <GREEN>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET> * 6_F
+ <RED>|<RESET> <GREEN>|<RESET><RED>_<RESET><YELLOW>|<RESET><RED>_<RESET><BLUE>|<RESET><RED>/<RESET><GREEN>|<RESET>
+ <RED>|<RESET><RED>/<RESET><GREEN>|<RESET> <YELLOW>|<RESET> <BLUE>|<RESET><GREEN>/<RESET>
+ <RED>|<RESET> <GREEN>|<RESET> <YELLOW>|<RESET><GREEN>/<RESET><BLUE>|<RESET>
+ <RED>|<RESET> <GREEN>|<RESET><GREEN>/<RESET><YELLOW>|<RESET> <BLUE>|<RESET>
+ <RED>|<RESET> <GREEN>|<RESET> * <BLUE>|<RESET> 6_E
+ <RED>|<RESET> * <CYAN>|<RESET> <BLUE>|<RESET> 6_D
+ <RED>|<RESET> <BLUE>|<RESET> <CYAN>|<RESET><BLUE>/<RESET>
+ <RED>|<RESET> <BLUE>|<RESET><BLUE>/<RESET><CYAN>|<RESET>
+ * <BLUE>|<RESET> <CYAN>|<RESET> 6_C
+ <CYAN>|<RESET> <BLUE>|<RESET><CYAN>/<RESET>
+ <CYAN>|<RESET><CYAN>/<RESET><BLUE>|<RESET>
+ * <BLUE>|<RESET> 6_B
+ <BLUE>|<RESET><BLUE>/<RESET>
+ * 6_A
+ EOF
+ git log --color=always --graph --date-order --pretty=tformat:%s 6_1 6_3 6_5 >actual.colors.raw &&
+ test_decode_color <actual.colors.raw | sed "s/ *\$//" >actual.colors &&
+ test_cmp expect.colors actual.colors
+'
+
test_done
diff --git a/t/t7415-submodule-names.sh b/t/t7415-submodule-names.sh
index 905a557585..f70368bc2e 100755
--- a/t/t7415-submodule-names.sh
+++ b/t/t7415-submodule-names.sh
@@ -207,6 +207,9 @@ test_expect_success MINGW 'prevent git~1 squatting on Windows' '
git hash-object -w --stdin)" &&
rev="$(git rev-parse --verify HEAD)" &&
hash="$(echo x | git hash-object -w --stdin)" &&
+ test_must_fail git update-index --add \
+ --cacheinfo 160000,$rev,d\\a 2>err &&
+ test_i18ngrep "Invalid path" err &&
git -c core.protectNTFS=false update-index --add \
--cacheinfo 100644,$modules,.gitmodules \
--cacheinfo 160000,$rev,c \
@@ -214,9 +217,7 @@ test_expect_success MINGW 'prevent git~1 squatting on Windows' '
--cacheinfo 100644,$hash,d./a/x \
--cacheinfo 100644,$hash,d./a/..git &&
test_tick &&
- git -c core.protectNTFS=false commit -m "module" &&
- test_must_fail git show HEAD: 2>err &&
- test_i18ngrep backslash err
+ git -c core.protectNTFS=false commit -m "module"
) &&
test_must_fail git -c core.protectNTFS=false \
clone --recurse-submodules squatting squatting-clone 2>err &&
diff --git a/tree-walk.c b/tree-walk.c
index b3d162051f..d5a8e096a6 100644
--- a/tree-walk.c
+++ b/tree-walk.c
@@ -43,12 +43,6 @@ static int decode_tree_entry(struct tree_desc *desc, const char *buf, unsigned l
strbuf_addstr(err, _("empty filename in tree entry"));
return -1;
}
-#ifdef GIT_WINDOWS_NATIVE
- if (protect_ntfs && strchr(path, '\\')) {
- strbuf_addf(err, _("filename in tree entry contains backslash: '%s'"), path);
- return -1;
- }
-#endif
len = strlen(path) + 1;
/* Initialize the descriptor entry */