mirror of
https://github.com/git-for-windows/git.git
synced 2026-02-03 18:59:59 -06:00
Add path walk API and its use in 'git pack-objects' (#5171)
This is a follow up to #5157 as well as motivated by the RFC in gitgitgadget/git#1786. We have ways of walking all objects, but it is focused on visiting a single commit and then expanding the new trees and blobs reachable from that commit that have not been visited yet. This means that objects arrive without any locality based on their path. Add a new "path walk API" that focuses on walking objects in batches according to their type and path. This will walk all annotated tags, all commits, all root trees, and then start a depth-first search among all paths in the repo to collect trees and blobs in batches. The most important application for this is being fast-tracked to Git for Windows: `git pack-objects --path-walk`. This application of the path walk API discovers the objects to pack via this batched walk, and automatically groups objects that appear at a common path so they can be checked for delta comparisons. This use completely avoids any name-hash collisions (even the collisions that sometimes occur with the new `--full-name-hash` option) and can be much faster to compute since the first pass of delta calculations does not waste time on objects that are unlikely to be diffable. Some statistics are available in the commit messages.
This commit is contained in:
commit
773bd460e9
@ -20,6 +20,10 @@ walking fewer objects.
|
||||
+
|
||||
* `pack.allowPackReuse=multi` may improve the time it takes to create a pack by
|
||||
reusing objects from multiple packs instead of just one.
|
||||
+
|
||||
* `pack.usePathWalk` may speed up packfile creation and make the packfiles be
|
||||
significantly smaller in the presence of certain filename collisions with Git's
|
||||
default name-hash.
|
||||
|
||||
feature.manyFiles::
|
||||
Enable config options that optimize for repos with many files in the
|
||||
|
||||
@ -155,6 +155,14 @@ pack.useSparse::
|
||||
commits contain certain types of direct renames. Default is
|
||||
`true`.
|
||||
|
||||
pack.usePathWalk::
|
||||
When true, git will default to using the '--path-walk' option in
|
||||
'git pack-objects' when the '--revs' option is present. This
|
||||
algorithm groups objects by path to maximize the ability to
|
||||
compute delta chains across historical versions of the same
|
||||
object. This may disable other options, such as using bitmaps to
|
||||
enumerate objects.
|
||||
|
||||
pack.preferBitmapTips::
|
||||
When selecting which commits will receive bitmaps, prefer a
|
||||
commit at the tip of any reference that is a suffix of any value
|
||||
|
||||
@ -16,7 +16,7 @@ SYNOPSIS
|
||||
[--cruft] [--cruft-expiration=<time>]
|
||||
[--stdout [--filter=<filter-spec>] | <base-name>]
|
||||
[--shallow] [--keep-true-parents] [--[no-]sparse]
|
||||
[--full-name-hash] < <object-list>
|
||||
[--full-name-hash] [--path-walk] < <object-list>
|
||||
|
||||
|
||||
DESCRIPTION
|
||||
@ -346,6 +346,16 @@ raise an error.
|
||||
Restrict delta matches based on "islands". See DELTA ISLANDS
|
||||
below.
|
||||
|
||||
--path-walk::
|
||||
By default, `git pack-objects` walks objects in an order that
|
||||
presents trees and blobs in an order unrelated to the path they
|
||||
appear relative to a commit's root tree. The `--path-walk` option
|
||||
enables a different walking algorithm that organizes trees and
|
||||
blobs by path. This has the potential to improve delta compression
|
||||
especially in the presence of filenames that cause collisions in
|
||||
Git's default name-hash algorithm. Due to changing how the objects
|
||||
are walked, this option is not compatible with `--delta-islands`,
|
||||
`--shallow`, or `--filter`.
|
||||
|
||||
DELTA ISLANDS
|
||||
-------------
|
||||
|
||||
@ -11,7 +11,7 @@ SYNOPSIS
|
||||
[verse]
|
||||
'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]
|
||||
[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]
|
||||
[--write-midx] [--full-name-hash]
|
||||
[--write-midx] [--full-name-hash] [--path-walk]
|
||||
|
||||
DESCRIPTION
|
||||
-----------
|
||||
@ -251,6 +251,19 @@ linkgit:git-multi-pack-index[1]).
|
||||
Write a multi-pack index (see linkgit:git-multi-pack-index[1])
|
||||
containing the non-redundant packs.
|
||||
|
||||
--path-walk::
|
||||
This option passes the `--path-walk` option to the underlying
|
||||
`git pack-options` process (see linkgit:git-pack-objects[1]).
|
||||
By default, `git pack-objects` walks objects in an order that
|
||||
presents trees and blobs in an order unrelated to the path they
|
||||
appear relative to a commit's root tree. The `--path-walk` option
|
||||
enables a different walking algorithm that organizes trees and
|
||||
blobs by path. This has the potential to improve delta compression
|
||||
especially in the presence of filenames that cause collisions in
|
||||
Git's default name-hash algorithm. Due to changing how the objects
|
||||
are walked, this option is not compatible with `--delta-islands`
|
||||
or `--filter`.
|
||||
|
||||
CONFIGURATION
|
||||
-------------
|
||||
|
||||
|
||||
73
Documentation/technical/api-path-walk.txt
Normal file
73
Documentation/technical/api-path-walk.txt
Normal file
@ -0,0 +1,73 @@
|
||||
Path-Walk API
|
||||
=============
|
||||
|
||||
The path-walk API is used to walk reachable objects, but to visit objects
|
||||
in batches based on a common path they appear in, or by type.
|
||||
|
||||
For example, all reachable commits are visited in a group. All tags are
|
||||
visited in a group. Then, all root trees are visited. At some point, all
|
||||
blobs reachable via a path `my/dir/to/A` are visited. When there are
|
||||
multiple paths possible to reach the same object, then only one of those
|
||||
paths is used to visit the object.
|
||||
|
||||
When walking a range of commits with some `UNINTERESTING` objects, the
|
||||
objects with the `UNINTERESTING` flag are included in these batches. In
|
||||
order to walk `UNINTERESTING` objects, the `--boundary` option must be
|
||||
used in the commit walk in order to visit `UNINTERESTING` commits.
|
||||
|
||||
Basics
|
||||
------
|
||||
|
||||
To use the path-walk API, include `path-walk.h` and call
|
||||
`walk_objects_by_path()` with a customized `path_walk_info` struct. The
|
||||
struct is used to set all of the options for how the walk should proceed.
|
||||
Let's dig into the different options and their use.
|
||||
|
||||
`path_fn` and `path_fn_data`::
|
||||
The most important option is the `path_fn` option, which is a
|
||||
function pointer to the callback that can execute logic on the
|
||||
object IDs for objects grouped by type and path. This function
|
||||
also receives a `data` value that corresponds to the
|
||||
`path_fn_data` member, for providing custom data structures to
|
||||
this callback function.
|
||||
|
||||
`revs`::
|
||||
To configure the exact details of the reachable set of objects,
|
||||
use the `revs` member and initialize it using the revision
|
||||
machinery in `revision.h`. Initialize `revs` using calls such as
|
||||
`setup_revisions()` or `parse_revision_opt()`. Do not call
|
||||
`prepare_revision_walk()`, as that will be called within
|
||||
`walk_objects_by_path()`.
|
||||
+
|
||||
It is also important that you do not specify the `--objects` flag for the
|
||||
`revs` struct. The revision walk should only be used to walk commits, and
|
||||
the objects will be walked in a separate way based on those starting
|
||||
commits.
|
||||
+
|
||||
If you want the path-walk API to emit `UNINTERESTING` objects based on the
|
||||
commit walk's boundary, be sure to set `revs.boundary` so the boundary
|
||||
commits are emitted.
|
||||
|
||||
`commits`, `blobs`, `trees`, `tags`::
|
||||
By default, these members are enabled and signal that the path-walk
|
||||
API should call the `path_fn` on objects of these types. Specialized
|
||||
applications could disable some options to make it simpler to walk
|
||||
the objects or to have fewer calls to `path_fn`.
|
||||
+
|
||||
While it is possible to walk only commits in this way, consumers would be
|
||||
better off using the revision walk API instead.
|
||||
|
||||
`prune_all_uninteresting`::
|
||||
By default, all reachable paths are emitted by the path-walk API.
|
||||
This option allows consumers to declare that they are not
|
||||
interested in paths where all included objects are marked with the
|
||||
`UNINTERESTING` flag. This requires using the `boundary` option in
|
||||
the revision walk so that the walk emits commits marked with the
|
||||
`UNINTERESTING` flag.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
See example usages in:
|
||||
`t/helper/test-path-walk.c`,
|
||||
`builtin/pack-objects.c`
|
||||
2
Makefile
2
Makefile
@ -828,6 +828,7 @@ TEST_BUILTINS_OBJS += test-parse-options.o
|
||||
TEST_BUILTINS_OBJS += test-parse-pathspec-file.o
|
||||
TEST_BUILTINS_OBJS += test-partial-clone.o
|
||||
TEST_BUILTINS_OBJS += test-path-utils.o
|
||||
TEST_BUILTINS_OBJS += test-path-walk.o
|
||||
TEST_BUILTINS_OBJS += test-pcre2-config.o
|
||||
TEST_BUILTINS_OBJS += test-pkt-line.o
|
||||
TEST_BUILTINS_OBJS += test-proc-receive.o
|
||||
@ -1104,6 +1105,7 @@ LIB_OBJS += parse-options.o
|
||||
LIB_OBJS += patch-delta.o
|
||||
LIB_OBJS += patch-ids.o
|
||||
LIB_OBJS += path.o
|
||||
LIB_OBJS += path-walk.o
|
||||
LIB_OBJS += pathspec.o
|
||||
LIB_OBJS += pkt-line.o
|
||||
LIB_OBJS += preload-index.o
|
||||
|
||||
@ -39,6 +39,9 @@
|
||||
#include "promisor-remote.h"
|
||||
#include "pack-mtimes.h"
|
||||
#include "parse-options.h"
|
||||
#include "blob.h"
|
||||
#include "tree.h"
|
||||
#include "path-walk.h"
|
||||
|
||||
/*
|
||||
* Objects we are going to pack are collected in the `to_pack` structure.
|
||||
@ -215,6 +218,7 @@ static int delta_search_threads;
|
||||
static int pack_to_stdout;
|
||||
static int sparse;
|
||||
static int thin;
|
||||
static int path_walk = -1;
|
||||
static int num_preferred_base;
|
||||
static struct progress *progress_state;
|
||||
|
||||
@ -2933,6 +2937,7 @@ static void find_deltas(struct object_entry **list, unsigned *list_size,
|
||||
struct thread_params {
|
||||
pthread_t thread;
|
||||
struct object_entry **list;
|
||||
struct packing_region *regions;
|
||||
unsigned list_size;
|
||||
unsigned remaining;
|
||||
int window;
|
||||
@ -3175,6 +3180,234 @@ static int add_ref_tag(const char *tag UNUSED, const char *referent UNUSED, cons
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int should_attempt_deltas(struct object_entry *entry)
|
||||
{
|
||||
if (DELTA(entry))
|
||||
return 0;
|
||||
|
||||
if (!entry->type_valid ||
|
||||
oe_size_less_than(&to_pack, entry, 50))
|
||||
return 0;
|
||||
|
||||
if (entry->no_try_delta)
|
||||
return 0;
|
||||
|
||||
if (!entry->preferred_base) {
|
||||
if (oe_type(entry) < 0)
|
||||
die(_("unable to get type of object %s"),
|
||||
oid_to_hex(&entry->idx.oid));
|
||||
} else if (oe_type(entry) < 0) {
|
||||
/*
|
||||
* This object is not found, but we
|
||||
* don't have to include it anyway.
|
||||
*/
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void find_deltas_for_region(struct object_entry *list UNUSED,
|
||||
struct packing_region *region,
|
||||
unsigned int *processed)
|
||||
{
|
||||
struct object_entry **delta_list;
|
||||
uint32_t delta_list_nr = 0;
|
||||
|
||||
ALLOC_ARRAY(delta_list, region->nr);
|
||||
for (uint32_t i = 0; i < region->nr; i++) {
|
||||
struct object_entry *entry = to_pack.objects + region->start + i;
|
||||
if (should_attempt_deltas(entry))
|
||||
delta_list[delta_list_nr++] = entry;
|
||||
}
|
||||
|
||||
QSORT(delta_list, delta_list_nr, type_size_sort);
|
||||
find_deltas(delta_list, &delta_list_nr, window, depth, processed);
|
||||
free(delta_list);
|
||||
}
|
||||
|
||||
static void find_deltas_by_region(struct object_entry *list,
|
||||
struct packing_region *regions,
|
||||
uint32_t start, uint32_t nr)
|
||||
{
|
||||
unsigned int processed = 0;
|
||||
uint32_t progress_nr;
|
||||
|
||||
if (!nr)
|
||||
return;
|
||||
|
||||
progress_nr = regions[nr - 1].start + regions[nr - 1].nr;
|
||||
|
||||
if (progress)
|
||||
progress_state = start_progress(_("Compressing objects by path"),
|
||||
progress_nr);
|
||||
|
||||
while (nr--)
|
||||
find_deltas_for_region(list,
|
||||
®ions[start++],
|
||||
&processed);
|
||||
|
||||
display_progress(progress_state, progress_nr);
|
||||
stop_progress(&progress_state);
|
||||
}
|
||||
|
||||
static void *threaded_find_deltas_by_path(void *arg)
|
||||
{
|
||||
struct thread_params *me = arg;
|
||||
|
||||
progress_lock();
|
||||
while (me->remaining) {
|
||||
while (me->remaining) {
|
||||
progress_unlock();
|
||||
find_deltas_for_region(to_pack.objects,
|
||||
me->regions,
|
||||
me->processed);
|
||||
progress_lock();
|
||||
me->remaining--;
|
||||
me->regions++;
|
||||
}
|
||||
|
||||
me->working = 0;
|
||||
pthread_cond_signal(&progress_cond);
|
||||
progress_unlock();
|
||||
|
||||
/*
|
||||
* We must not set ->data_ready before we wait on the
|
||||
* condition because the main thread may have set it to 1
|
||||
* before we get here. In order to be sure that new
|
||||
* work is available if we see 1 in ->data_ready, it
|
||||
* was initialized to 0 before this thread was spawned
|
||||
* and we reset it to 0 right away.
|
||||
*/
|
||||
pthread_mutex_lock(&me->mutex);
|
||||
while (!me->data_ready)
|
||||
pthread_cond_wait(&me->cond, &me->mutex);
|
||||
me->data_ready = 0;
|
||||
pthread_mutex_unlock(&me->mutex);
|
||||
|
||||
progress_lock();
|
||||
}
|
||||
progress_unlock();
|
||||
/* leave ->working 1 so that this doesn't get more work assigned */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void ll_find_deltas_by_region(struct object_entry *list,
|
||||
struct packing_region *regions,
|
||||
uint32_t start, uint32_t nr)
|
||||
{
|
||||
struct thread_params *p;
|
||||
int i, ret, active_threads = 0;
|
||||
unsigned int processed = 0;
|
||||
uint32_t progress_nr;
|
||||
init_threaded_search();
|
||||
|
||||
if (!nr)
|
||||
return;
|
||||
|
||||
progress_nr = regions[nr - 1].start + regions[nr - 1].nr;
|
||||
if (delta_search_threads <= 1) {
|
||||
find_deltas_by_region(list, regions, start, nr);
|
||||
cleanup_threaded_search();
|
||||
return;
|
||||
}
|
||||
|
||||
if (progress > pack_to_stdout)
|
||||
fprintf_ln(stderr, _("Path-based delta compression using up to %d threads"),
|
||||
delta_search_threads);
|
||||
CALLOC_ARRAY(p, delta_search_threads);
|
||||
|
||||
if (progress)
|
||||
progress_state = start_progress(_("Compressing objects by path"),
|
||||
progress_nr);
|
||||
/* Partition the work amongst work threads. */
|
||||
for (i = 0; i < delta_search_threads; i++) {
|
||||
unsigned sub_size = nr / (delta_search_threads - i);
|
||||
|
||||
p[i].window = window;
|
||||
p[i].depth = depth;
|
||||
p[i].processed = &processed;
|
||||
p[i].working = 1;
|
||||
p[i].data_ready = 0;
|
||||
|
||||
p[i].regions = regions;
|
||||
p[i].list_size = sub_size;
|
||||
p[i].remaining = sub_size;
|
||||
|
||||
regions += sub_size;
|
||||
nr -= sub_size;
|
||||
}
|
||||
|
||||
/* Start work threads. */
|
||||
for (i = 0; i < delta_search_threads; i++) {
|
||||
if (!p[i].list_size)
|
||||
continue;
|
||||
pthread_mutex_init(&p[i].mutex, NULL);
|
||||
pthread_cond_init(&p[i].cond, NULL);
|
||||
ret = pthread_create(&p[i].thread, NULL,
|
||||
threaded_find_deltas_by_path, &p[i]);
|
||||
if (ret)
|
||||
die(_("unable to create thread: %s"), strerror(ret));
|
||||
active_threads++;
|
||||
}
|
||||
|
||||
/*
|
||||
* Now let's wait for work completion. Each time a thread is done
|
||||
* with its work, we steal half of the remaining work from the
|
||||
* thread with the largest number of unprocessed objects and give
|
||||
* it to that newly idle thread. This ensure good load balancing
|
||||
* until the remaining object list segments are simply too short
|
||||
* to be worth splitting anymore.
|
||||
*/
|
||||
while (active_threads) {
|
||||
struct thread_params *target = NULL;
|
||||
struct thread_params *victim = NULL;
|
||||
unsigned sub_size = 0;
|
||||
|
||||
progress_lock();
|
||||
for (;;) {
|
||||
for (i = 0; !target && i < delta_search_threads; i++)
|
||||
if (!p[i].working)
|
||||
target = &p[i];
|
||||
if (target)
|
||||
break;
|
||||
pthread_cond_wait(&progress_cond, &progress_mutex);
|
||||
}
|
||||
|
||||
for (i = 0; i < delta_search_threads; i++)
|
||||
if (p[i].remaining > 2*window &&
|
||||
(!victim || victim->remaining < p[i].remaining))
|
||||
victim = &p[i];
|
||||
if (victim) {
|
||||
sub_size = victim->remaining / 2;
|
||||
target->regions = victim->regions + victim->remaining - sub_size;
|
||||
victim->list_size -= sub_size;
|
||||
victim->remaining -= sub_size;
|
||||
}
|
||||
target->list_size = sub_size;
|
||||
target->remaining = sub_size;
|
||||
target->working = 1;
|
||||
progress_unlock();
|
||||
|
||||
pthread_mutex_lock(&target->mutex);
|
||||
target->data_ready = 1;
|
||||
pthread_cond_signal(&target->cond);
|
||||
pthread_mutex_unlock(&target->mutex);
|
||||
|
||||
if (!sub_size) {
|
||||
pthread_join(target->thread, NULL);
|
||||
pthread_cond_destroy(&target->cond);
|
||||
pthread_mutex_destroy(&target->mutex);
|
||||
active_threads--;
|
||||
}
|
||||
}
|
||||
cleanup_threaded_search();
|
||||
free(p);
|
||||
|
||||
display_progress(progress_state, progress_nr);
|
||||
stop_progress(&progress_state);
|
||||
}
|
||||
|
||||
static void prepare_pack(int window, int depth)
|
||||
{
|
||||
struct object_entry **delta_list;
|
||||
@ -3199,39 +3432,21 @@ static void prepare_pack(int window, int depth)
|
||||
if (!to_pack.nr_objects || !window || !depth)
|
||||
return;
|
||||
|
||||
if (path_walk)
|
||||
ll_find_deltas_by_region(to_pack.objects, to_pack.regions,
|
||||
0, to_pack.nr_regions);
|
||||
|
||||
ALLOC_ARRAY(delta_list, to_pack.nr_objects);
|
||||
nr_deltas = n = 0;
|
||||
|
||||
for (i = 0; i < to_pack.nr_objects; i++) {
|
||||
struct object_entry *entry = to_pack.objects + i;
|
||||
|
||||
if (DELTA(entry))
|
||||
/* This happens if we decided to reuse existing
|
||||
* delta from a pack. "reuse_delta &&" is implied.
|
||||
*/
|
||||
if (!should_attempt_deltas(entry))
|
||||
continue;
|
||||
|
||||
if (!entry->type_valid ||
|
||||
oe_size_less_than(&to_pack, entry, 50))
|
||||
continue;
|
||||
|
||||
if (entry->no_try_delta)
|
||||
continue;
|
||||
|
||||
if (!entry->preferred_base) {
|
||||
if (!entry->preferred_base)
|
||||
nr_deltas++;
|
||||
if (oe_type(entry) < 0)
|
||||
die(_("unable to get type of object %s"),
|
||||
oid_to_hex(&entry->idx.oid));
|
||||
} else {
|
||||
if (oe_type(entry) < 0) {
|
||||
/*
|
||||
* This object is not found, but we
|
||||
* don't have to include it anyway.
|
||||
*/
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
delta_list[n++] = entry;
|
||||
}
|
||||
@ -4146,6 +4361,88 @@ static void mark_bitmap_preferred_tips(void)
|
||||
}
|
||||
}
|
||||
|
||||
static inline int is_oid_interesting(struct repository *repo,
|
||||
struct object_id *oid)
|
||||
{
|
||||
struct object *o = lookup_object(repo, oid);
|
||||
return o && !(o->flags & UNINTERESTING);
|
||||
}
|
||||
|
||||
static int add_objects_by_path(const char *path,
|
||||
struct oid_array *oids,
|
||||
enum object_type type,
|
||||
void *data)
|
||||
{
|
||||
size_t oe_start = to_pack.nr_objects;
|
||||
size_t oe_end;
|
||||
unsigned int *processed = data;
|
||||
|
||||
/*
|
||||
* First, add all objects to the packing data, including the ones
|
||||
* marked UNINTERESTING (translated to 'exclude') as they can be
|
||||
* used as delta bases.
|
||||
*/
|
||||
for (size_t i = 0; i < oids->nr; i++) {
|
||||
int exclude;
|
||||
struct object_info oi = OBJECT_INFO_INIT;
|
||||
struct object_id *oid = &oids->oid[i];
|
||||
|
||||
/* Skip objects that do not exist locally. */
|
||||
if ((exclude_promisor_objects || arg_missing_action != MA_ERROR) &&
|
||||
oid_object_info_extended(the_repository, oid, &oi,
|
||||
OBJECT_INFO_FOR_PREFETCH) < 0)
|
||||
continue;
|
||||
|
||||
exclude = !is_oid_interesting(the_repository, oid);
|
||||
|
||||
if (exclude && !thin)
|
||||
continue;
|
||||
|
||||
add_object_entry(oid, type, path, exclude);
|
||||
}
|
||||
|
||||
oe_end = to_pack.nr_objects;
|
||||
|
||||
/* We can skip delta calculations if it is a no-op. */
|
||||
if (oe_end == oe_start || !window)
|
||||
return 0;
|
||||
|
||||
ALLOC_GROW(to_pack.regions,
|
||||
to_pack.nr_regions + 1,
|
||||
to_pack.nr_regions_alloc);
|
||||
|
||||
to_pack.regions[to_pack.nr_regions].start = oe_start;
|
||||
to_pack.regions[to_pack.nr_regions].nr = oe_end - oe_start;
|
||||
to_pack.nr_regions++;
|
||||
|
||||
*processed += oids->nr;
|
||||
display_progress(progress_state, *processed);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void get_object_list_path_walk(struct rev_info *revs)
|
||||
{
|
||||
struct path_walk_info info = PATH_WALK_INFO_INIT;
|
||||
unsigned int processed = 0;
|
||||
|
||||
info.revs = revs;
|
||||
info.path_fn = add_objects_by_path;
|
||||
info.path_fn_data = &processed;
|
||||
revs->tag_objects = 1;
|
||||
|
||||
/*
|
||||
* Allow the --[no-]sparse option to be interesting here, if only
|
||||
* for testing purposes. Paths with no interesting objects will not
|
||||
* contribute to the resulting pack, but only create noisy preferred
|
||||
* base objects.
|
||||
*/
|
||||
info.prune_all_uninteresting = sparse;
|
||||
|
||||
if (walk_objects_by_path(&info))
|
||||
die(_("failed to pack objects via path-walk"));
|
||||
}
|
||||
|
||||
static void get_object_list(struct rev_info *revs, int ac, const char **av)
|
||||
{
|
||||
struct setup_revision_opt s_r_opt = {
|
||||
@ -4192,7 +4489,7 @@ static void get_object_list(struct rev_info *revs, int ac, const char **av)
|
||||
|
||||
warn_on_object_refname_ambiguity = save_warning;
|
||||
|
||||
if (use_bitmap_index && !get_object_list_from_bitmap(revs))
|
||||
if (use_bitmap_index && !path_walk && !get_object_list_from_bitmap(revs))
|
||||
return;
|
||||
|
||||
if (use_delta_islands)
|
||||
@ -4201,15 +4498,19 @@ static void get_object_list(struct rev_info *revs, int ac, const char **av)
|
||||
if (write_bitmap_index)
|
||||
mark_bitmap_preferred_tips();
|
||||
|
||||
if (prepare_revision_walk(revs))
|
||||
die(_("revision walk setup failed"));
|
||||
mark_edges_uninteresting(revs, show_edge, sparse);
|
||||
|
||||
if (!fn_show_object)
|
||||
fn_show_object = show_object;
|
||||
traverse_commit_list(revs,
|
||||
show_commit, fn_show_object,
|
||||
NULL);
|
||||
|
||||
if (path_walk) {
|
||||
get_object_list_path_walk(revs);
|
||||
} else {
|
||||
if (prepare_revision_walk(revs))
|
||||
die(_("revision walk setup failed"));
|
||||
mark_edges_uninteresting(revs, show_edge, sparse);
|
||||
traverse_commit_list(revs,
|
||||
show_commit, fn_show_object,
|
||||
NULL);
|
||||
}
|
||||
|
||||
if (unpack_unreachable_expiration) {
|
||||
revs->ignore_missing_links = 1;
|
||||
@ -4407,6 +4708,8 @@ int cmd_pack_objects(int argc,
|
||||
N_("use the sparse reachability algorithm")),
|
||||
OPT_BOOL(0, "thin", &thin,
|
||||
N_("create thin packs")),
|
||||
OPT_BOOL(0, "path-walk", &path_walk,
|
||||
N_("use the path-walk API to walk objects when possible")),
|
||||
OPT_BOOL(0, "shallow", &shallow,
|
||||
N_("create packs suitable for shallow fetches")),
|
||||
OPT_BOOL(0, "honor-pack-keep", &ignore_packed_keep_on_disk,
|
||||
@ -4473,6 +4776,17 @@ int cmd_pack_objects(int argc,
|
||||
if (pack_to_stdout != !base_name || argc)
|
||||
usage_with_options(pack_usage, pack_objects_options);
|
||||
|
||||
if (path_walk < 0) {
|
||||
if (use_bitmap_index > 0 ||
|
||||
!use_internal_rev_list)
|
||||
path_walk = 0;
|
||||
else if (the_repository->gitdir &&
|
||||
the_repository->settings.pack_use_path_walk)
|
||||
path_walk = 1;
|
||||
else
|
||||
path_walk = git_env_bool("GIT_TEST_PACK_PATH_WALK", 0);
|
||||
}
|
||||
|
||||
if (depth < 0)
|
||||
depth = 0;
|
||||
if (depth >= (1 << OE_DEPTH_BITS)) {
|
||||
@ -4489,7 +4803,27 @@ int cmd_pack_objects(int argc,
|
||||
window = 0;
|
||||
|
||||
strvec_push(&rp, "pack-objects");
|
||||
if (thin) {
|
||||
|
||||
if (path_walk && filter_options.choice) {
|
||||
warning(_("cannot use --filter with --path-walk"));
|
||||
path_walk = 0;
|
||||
}
|
||||
if (path_walk && use_delta_islands) {
|
||||
warning(_("cannot use delta islands with --path-walk"));
|
||||
path_walk = 0;
|
||||
}
|
||||
if (path_walk && shallow) {
|
||||
warning(_("cannot use --shallow with --path-walk"));
|
||||
path_walk = 0;
|
||||
}
|
||||
if (path_walk) {
|
||||
strvec_push(&rp, "--boundary");
|
||||
/*
|
||||
* We must disable the bitmaps because we are removing
|
||||
* the --objects / --objects-edge[-aggressive] options.
|
||||
*/
|
||||
use_bitmap_index = 0;
|
||||
} else if (thin) {
|
||||
use_internal_rev_list = 1;
|
||||
strvec_push(&rp, shallow
|
||||
? "--objects-edge-aggressive"
|
||||
|
||||
@ -41,7 +41,7 @@ static char *packdir, *packtmp_name, *packtmp;
|
||||
static const char *const git_repack_usage[] = {
|
||||
N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n"
|
||||
"[--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]\n"
|
||||
"[--write-midx] [--full-name-hash]"),
|
||||
"[--write-midx] [--full-name-hash] [--path-walk]"),
|
||||
NULL
|
||||
};
|
||||
|
||||
@ -61,6 +61,7 @@ struct pack_objects_args {
|
||||
int quiet;
|
||||
int local;
|
||||
int full_name_hash;
|
||||
int path_walk;
|
||||
struct list_objects_filter_options filter_options;
|
||||
};
|
||||
|
||||
@ -311,6 +312,8 @@ static void prepare_pack_objects(struct child_process *cmd,
|
||||
strvec_pushf(&cmd->args, "--no-reuse-object");
|
||||
if (args->full_name_hash)
|
||||
strvec_pushf(&cmd->args, "--full-name-hash");
|
||||
if (args->path_walk)
|
||||
strvec_pushf(&cmd->args, "--path-walk");
|
||||
if (args->local)
|
||||
strvec_push(&cmd->args, "--local");
|
||||
if (args->quiet)
|
||||
@ -1210,6 +1213,8 @@ int cmd_repack(int argc,
|
||||
N_("pass --no-reuse-object to git-pack-objects")),
|
||||
OPT_BOOL(0, "full-name-hash", &po_args.full_name_hash,
|
||||
N_("(EXPERIMENTAL!) pass --full-name-hash to git-pack-objects")),
|
||||
OPT_BOOL(0, "path-walk", &po_args.path_walk,
|
||||
N_("(EXPERIMENTAL!) pass --path-walk to git-pack-objects")),
|
||||
OPT_NEGBIT('n', NULL, &run_update_server_info,
|
||||
N_("do not run git-update-server-info"), 1),
|
||||
OPT__QUIET(&po_args.quiet, N_("be quiet")),
|
||||
|
||||
@ -26,6 +26,7 @@ linux-TEST-vars)
|
||||
export GIT_TEST_CHECKOUT_WORKERS=2
|
||||
export GIT_TEST_PACK_USE_BITMAP_BOUNDARY_TRAVERSAL=1
|
||||
export GIT_TEST_FULL_NAME_HASH=1
|
||||
export GIT_TEST_PACK_PATH_WALK=1
|
||||
;;
|
||||
linux-clang)
|
||||
export GIT_TEST_DEFAULT_HASH=sha1
|
||||
|
||||
@ -118,11 +118,23 @@ struct object_entry {
|
||||
unsigned ext_base:1; /* delta_idx points outside packlist */
|
||||
};
|
||||
|
||||
/**
|
||||
* A packing region is a section of the packing_data.objects array
|
||||
* as given by a starting index and a number of elements.
|
||||
*/
|
||||
struct packing_region {
|
||||
uint32_t start;
|
||||
uint32_t nr;
|
||||
};
|
||||
|
||||
struct packing_data {
|
||||
struct repository *repo;
|
||||
struct object_entry *objects;
|
||||
uint32_t nr_objects, nr_alloc;
|
||||
|
||||
struct packing_region *regions;
|
||||
uint32_t nr_regions, nr_regions_alloc;
|
||||
|
||||
int32_t *index;
|
||||
uint32_t index_size;
|
||||
|
||||
|
||||
399
path-walk.c
Normal file
399
path-walk.c
Normal file
@ -0,0 +1,399 @@
|
||||
/*
|
||||
* path-walk.c: implementation for path-based walks of the object graph.
|
||||
*/
|
||||
#include "git-compat-util.h"
|
||||
#include "path-walk.h"
|
||||
#include "blob.h"
|
||||
#include "commit.h"
|
||||
#include "dir.h"
|
||||
#include "hashmap.h"
|
||||
#include "hex.h"
|
||||
#include "object.h"
|
||||
#include "oid-array.h"
|
||||
#include "revision.h"
|
||||
#include "string-list.h"
|
||||
#include "strmap.h"
|
||||
#include "tag.h"
|
||||
#include "trace2.h"
|
||||
#include "tree.h"
|
||||
#include "tree-walk.h"
|
||||
|
||||
struct type_and_oid_list
|
||||
{
|
||||
enum object_type type;
|
||||
struct oid_array oids;
|
||||
int maybe_interesting;
|
||||
};
|
||||
|
||||
#define TYPE_AND_OID_LIST_INIT { \
|
||||
.type = OBJ_NONE, \
|
||||
.oids = OID_ARRAY_INIT \
|
||||
}
|
||||
|
||||
struct path_walk_context {
|
||||
/**
|
||||
* Repeats of data in 'struct path_walk_info' for
|
||||
* access with fewer characters.
|
||||
*/
|
||||
struct repository *repo;
|
||||
struct rev_info *revs;
|
||||
struct path_walk_info *info;
|
||||
|
||||
/**
|
||||
* Map a path to a 'struct type_and_oid_list'
|
||||
* containing the objects discovered at that
|
||||
* path.
|
||||
*/
|
||||
struct strmap paths_to_lists;
|
||||
|
||||
/**
|
||||
* Store the current list of paths in a stack, to
|
||||
* facilitate depth-first-search without recursion.
|
||||
*/
|
||||
struct string_list path_stack;
|
||||
};
|
||||
|
||||
static int add_children(struct path_walk_context *ctx,
|
||||
const char *base_path,
|
||||
struct object_id *oid)
|
||||
{
|
||||
struct tree_desc desc;
|
||||
struct name_entry entry;
|
||||
struct strbuf path = STRBUF_INIT;
|
||||
size_t base_len;
|
||||
struct tree *tree = lookup_tree(ctx->repo, oid);
|
||||
|
||||
if (!tree) {
|
||||
error(_("failed to walk children of tree %s: not found"),
|
||||
oid_to_hex(oid));
|
||||
return -1;
|
||||
} else if (parse_tree_gently(tree, 1)) {
|
||||
die("bad tree object %s", oid_to_hex(oid));
|
||||
}
|
||||
|
||||
strbuf_addstr(&path, base_path);
|
||||
base_len = path.len;
|
||||
|
||||
parse_tree(tree);
|
||||
init_tree_desc(&desc, &tree->object.oid, tree->buffer, tree->size);
|
||||
while (tree_entry(&desc, &entry)) {
|
||||
struct type_and_oid_list *list;
|
||||
struct object *o;
|
||||
/* Not actually true, but we will ignore submodules later. */
|
||||
enum object_type type = S_ISDIR(entry.mode) ? OBJ_TREE : OBJ_BLOB;
|
||||
|
||||
/* Skip submodules. */
|
||||
if (S_ISGITLINK(entry.mode))
|
||||
continue;
|
||||
|
||||
/* If the caller doesn't want blobs, then don't bother. */
|
||||
if (!ctx->info->blobs && type == OBJ_BLOB)
|
||||
continue;
|
||||
|
||||
if (type == OBJ_TREE) {
|
||||
struct tree *child = lookup_tree(ctx->repo, &entry.oid);
|
||||
o = child ? &child->object : NULL;
|
||||
} else if (type == OBJ_BLOB) {
|
||||
struct blob *child = lookup_blob(ctx->repo, &entry.oid);
|
||||
o = child ? &child->object : NULL;
|
||||
} else {
|
||||
/* Wrong type? */
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!o) /* report error?*/
|
||||
continue;
|
||||
|
||||
/* Skip this object if already seen. */
|
||||
if (o->flags & SEEN)
|
||||
continue;
|
||||
o->flags |= SEEN;
|
||||
|
||||
strbuf_setlen(&path, base_len);
|
||||
strbuf_add(&path, entry.path, entry.pathlen);
|
||||
|
||||
/*
|
||||
* Trees will end with "/" for concatenation and distinction
|
||||
* from blobs at the same path.
|
||||
*/
|
||||
if (type == OBJ_TREE)
|
||||
strbuf_addch(&path, '/');
|
||||
|
||||
if (!(list = strmap_get(&ctx->paths_to_lists, path.buf))) {
|
||||
CALLOC_ARRAY(list, 1);
|
||||
list->type = type;
|
||||
strmap_put(&ctx->paths_to_lists, path.buf, list);
|
||||
string_list_append(&ctx->path_stack, path.buf);
|
||||
}
|
||||
if (!(o->flags & UNINTERESTING))
|
||||
list->maybe_interesting = 1;
|
||||
oid_array_append(&list->oids, &entry.oid);
|
||||
}
|
||||
|
||||
free_tree_buffer(tree);
|
||||
strbuf_release(&path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* For each path in paths_to_explore, walk the trees another level
|
||||
* and add any found blobs to the batch (but only if they exist and
|
||||
* haven't been added yet).
|
||||
*/
|
||||
static int walk_path(struct path_walk_context *ctx,
|
||||
const char *path)
|
||||
{
|
||||
struct type_and_oid_list *list;
|
||||
int ret = 0;
|
||||
|
||||
list = strmap_get(&ctx->paths_to_lists, path);
|
||||
|
||||
if (ctx->info->prune_all_uninteresting) {
|
||||
/*
|
||||
* This is true if all objects were UNINTERESTING
|
||||
* when added to the list.
|
||||
*/
|
||||
if (!list->maybe_interesting)
|
||||
return 0;
|
||||
|
||||
/*
|
||||
* But it's still possible that the objects were set
|
||||
* as UNINTERESTING after being added. Do a quick check.
|
||||
*/
|
||||
list->maybe_interesting = 0;
|
||||
for (size_t i = 0;
|
||||
!list->maybe_interesting && i < list->oids.nr;
|
||||
i++) {
|
||||
if (list->type == OBJ_TREE) {
|
||||
struct tree *t = lookup_tree(ctx->repo,
|
||||
&list->oids.oid[i]);
|
||||
if (t && !(t->object.flags & UNINTERESTING))
|
||||
list->maybe_interesting = 1;
|
||||
} else {
|
||||
struct blob *b = lookup_blob(ctx->repo,
|
||||
&list->oids.oid[i]);
|
||||
if (b && !(b->object.flags & UNINTERESTING))
|
||||
list->maybe_interesting = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* We have confirmed that all objects are UNINTERESTING. */
|
||||
if (!list->maybe_interesting)
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Evaluate function pointer on this data, if requested. */
|
||||
if ((list->type == OBJ_TREE && ctx->info->trees) ||
|
||||
(list->type == OBJ_BLOB && ctx->info->blobs))
|
||||
ret = ctx->info->path_fn(path, &list->oids, list->type,
|
||||
ctx->info->path_fn_data);
|
||||
|
||||
/* Expand data for children. */
|
||||
if (list->type == OBJ_TREE) {
|
||||
for (size_t i = 0; i < list->oids.nr; i++) {
|
||||
ret |= add_children(ctx,
|
||||
path,
|
||||
&list->oids.oid[i]);
|
||||
}
|
||||
}
|
||||
|
||||
oid_array_clear(&list->oids);
|
||||
strmap_remove(&ctx->paths_to_lists, path, 1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void clear_strmap(struct strmap *map)
|
||||
{
|
||||
struct hashmap_iter iter;
|
||||
struct strmap_entry *e;
|
||||
|
||||
hashmap_for_each_entry(&map->map, &iter, e, ent) {
|
||||
struct type_and_oid_list *list = e->value;
|
||||
oid_array_clear(&list->oids);
|
||||
}
|
||||
strmap_clear(map, 1);
|
||||
strmap_init(map);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the configuration of 'info', walk the commits based on 'info->revs' and
|
||||
* call 'info->path_fn' on each discovered path.
|
||||
*
|
||||
* Returns nonzero on an error.
|
||||
*/
|
||||
int walk_objects_by_path(struct path_walk_info *info)
|
||||
{
|
||||
const char *root_path = "";
|
||||
int ret = 0, has_uninteresting = 0;
|
||||
size_t commits_nr = 0, paths_nr = 0;
|
||||
struct commit *c;
|
||||
struct type_and_oid_list *root_tree_list;
|
||||
struct type_and_oid_list *commit_list;
|
||||
struct path_walk_context ctx = {
|
||||
.repo = info->revs->repo,
|
||||
.revs = info->revs,
|
||||
.info = info,
|
||||
.path_stack = STRING_LIST_INIT_DUP,
|
||||
.paths_to_lists = STRMAP_INIT
|
||||
};
|
||||
struct oidset root_tree_set = OIDSET_INIT;
|
||||
|
||||
trace2_region_enter("path-walk", "commit-walk", info->revs->repo);
|
||||
|
||||
CALLOC_ARRAY(commit_list, 1);
|
||||
commit_list->type = OBJ_COMMIT;
|
||||
|
||||
if (info->tags)
|
||||
info->revs->tag_objects = 1;
|
||||
|
||||
/* Insert a single list for the root tree into the paths. */
|
||||
CALLOC_ARRAY(root_tree_list, 1);
|
||||
root_tree_list->type = OBJ_TREE;
|
||||
root_tree_list->maybe_interesting = 1;
|
||||
strmap_put(&ctx.paths_to_lists, root_path, root_tree_list);
|
||||
|
||||
/*
|
||||
* Set these values before preparing the walk to catch
|
||||
* lightweight tags pointing to non-commits.
|
||||
*/
|
||||
info->revs->blob_objects = info->blobs;
|
||||
info->revs->tree_objects = info->trees;
|
||||
|
||||
if (prepare_revision_walk(info->revs))
|
||||
die(_("failed to setup revision walk"));
|
||||
|
||||
info->revs->blob_objects = info->revs->tree_objects = 0;
|
||||
|
||||
if (info->tags) {
|
||||
struct oid_array tagged_blob_list = OID_ARRAY_INIT;
|
||||
struct oid_array tags = OID_ARRAY_INIT;
|
||||
|
||||
trace2_region_enter("path-walk", "tag-walk", info->revs->repo);
|
||||
|
||||
/*
|
||||
* Walk any pending objects at this point, but they should only
|
||||
* be tags.
|
||||
*/
|
||||
for (size_t i = 0; i < info->revs->pending.nr; i++) {
|
||||
struct object_array_entry *pending = info->revs->pending.objects + i;
|
||||
struct object *obj = pending->item;
|
||||
|
||||
if (obj->type == OBJ_COMMIT)
|
||||
continue;
|
||||
|
||||
while (obj->type == OBJ_TAG) {
|
||||
struct tag *tag = lookup_tag(info->revs->repo,
|
||||
&obj->oid);
|
||||
if (oid_array_lookup(&tags, &obj->oid) < 0)
|
||||
oid_array_append(&tags, &obj->oid);
|
||||
obj = tag->tagged;
|
||||
}
|
||||
|
||||
switch (obj->type) {
|
||||
case OBJ_TREE:
|
||||
if (info->trees &&
|
||||
oid_array_lookup(&root_tree_list->oids, &obj->oid) < 0)
|
||||
oid_array_append(&root_tree_list->oids, &obj->oid);
|
||||
break;
|
||||
|
||||
case OBJ_BLOB:
|
||||
if (info->blobs &&
|
||||
oid_array_lookup(&tagged_blob_list, &obj->oid) < 0)
|
||||
oid_array_append(&tagged_blob_list, &obj->oid);
|
||||
break;
|
||||
|
||||
case OBJ_COMMIT:
|
||||
/* Make sure it is in the object walk */
|
||||
add_pending_object(info->revs, obj, "");
|
||||
break;
|
||||
|
||||
default:
|
||||
BUG("should not see any other type here");
|
||||
}
|
||||
}
|
||||
|
||||
info->path_fn("", &tags, OBJ_TAG, info->path_fn_data);
|
||||
|
||||
if (tagged_blob_list.nr && info->blobs)
|
||||
info->path_fn("/tagged-blobs", &tagged_blob_list, OBJ_BLOB,
|
||||
info->path_fn_data);
|
||||
|
||||
trace2_data_intmax("path-walk", ctx.repo, "tags", tags.nr);
|
||||
trace2_region_leave("path-walk", "tag-walk", info->revs->repo);
|
||||
oid_array_clear(&tags);
|
||||
oid_array_clear(&tagged_blob_list);
|
||||
}
|
||||
|
||||
while ((c = get_revision(info->revs))) {
|
||||
struct object_id *oid;
|
||||
struct tree *t;
|
||||
commits_nr++;
|
||||
|
||||
if (info->commits)
|
||||
oid_array_append(&commit_list->oids,
|
||||
&c->object.oid);
|
||||
|
||||
/* If we only care about commits, then skip trees. */
|
||||
if (!info->trees && !info->blobs)
|
||||
continue;
|
||||
|
||||
oid = get_commit_tree_oid(c);
|
||||
t = lookup_tree(info->revs->repo, oid);
|
||||
|
||||
if (t) {
|
||||
oidset_insert(&root_tree_set, oid);
|
||||
oid_array_append(&root_tree_list->oids, oid);
|
||||
} else {
|
||||
warning("could not find tree %s", oid_to_hex(oid));
|
||||
}
|
||||
|
||||
if (t && (c->object.flags & UNINTERESTING)) {
|
||||
t->object.flags |= UNINTERESTING;
|
||||
has_uninteresting = 1;
|
||||
}
|
||||
}
|
||||
|
||||
trace2_data_intmax("path-walk", ctx.repo, "commits", commits_nr);
|
||||
trace2_region_leave("path-walk", "commit-walk", info->revs->repo);
|
||||
|
||||
/* Track all commits. */
|
||||
if (info->commits)
|
||||
ret = info->path_fn("", &commit_list->oids, OBJ_COMMIT,
|
||||
info->path_fn_data);
|
||||
oid_array_clear(&commit_list->oids);
|
||||
free(commit_list);
|
||||
|
||||
/*
|
||||
* Before performing a DFS of our paths and emitting them as interesting,
|
||||
* do a full walk of the trees to distribute the UNINTERESTING bit. Use
|
||||
* the sparse algorithm if prune_all_uninteresting was set.
|
||||
*/
|
||||
if (has_uninteresting) {
|
||||
trace2_region_enter("path-walk", "uninteresting-walk", info->revs->repo);
|
||||
if (info->prune_all_uninteresting)
|
||||
mark_trees_uninteresting_sparse(ctx.repo, &root_tree_set);
|
||||
else
|
||||
mark_trees_uninteresting_dense(ctx.repo, &root_tree_set);
|
||||
trace2_region_leave("path-walk", "uninteresting-walk", info->revs->repo);
|
||||
}
|
||||
oidset_clear(&root_tree_set);
|
||||
|
||||
string_list_append(&ctx.path_stack, root_path);
|
||||
|
||||
trace2_region_enter("path-walk", "path-walk", info->revs->repo);
|
||||
while (!ret && ctx.path_stack.nr) {
|
||||
char *path = ctx.path_stack.items[ctx.path_stack.nr - 1].string;
|
||||
ctx.path_stack.nr--;
|
||||
paths_nr++;
|
||||
|
||||
ret = walk_path(&ctx, path);
|
||||
|
||||
free(path);
|
||||
}
|
||||
trace2_data_intmax("path-walk", ctx.repo, "paths", paths_nr);
|
||||
trace2_region_leave("path-walk", "path-walk", info->revs->repo);
|
||||
|
||||
clear_strmap(&ctx.paths_to_lists);
|
||||
string_list_clear(&ctx.path_stack, 0);
|
||||
return ret;
|
||||
}
|
||||
64
path-walk.h
Normal file
64
path-walk.h
Normal file
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* path-walk.h : Methods and structures for walking the object graph in batches
|
||||
* by the paths that can reach those objects.
|
||||
*/
|
||||
#include "object.h" /* Required for 'enum object_type'. */
|
||||
|
||||
struct rev_info;
|
||||
struct oid_array;
|
||||
|
||||
/**
|
||||
* The type of a function pointer for the method that is called on a list of
|
||||
* objects reachable at a given path.
|
||||
*/
|
||||
typedef int (*path_fn)(const char *path,
|
||||
struct oid_array *oids,
|
||||
enum object_type type,
|
||||
void *data);
|
||||
|
||||
struct path_walk_info {
|
||||
/**
|
||||
* revs provides the definitions for the commit walk, including
|
||||
* which commits are UNINTERESTING or not.
|
||||
*/
|
||||
struct rev_info *revs;
|
||||
|
||||
/**
|
||||
* The caller wishes to execute custom logic on objects reachable at a
|
||||
* given path. Every reachable object will be visited exactly once, and
|
||||
* the first path to see an object wins. This may not be a stable choice.
|
||||
*/
|
||||
path_fn path_fn;
|
||||
void *path_fn_data;
|
||||
/**
|
||||
* Initialize which object types the path_fn should be called on. This
|
||||
* could also limit the walk to skip blobs if not set.
|
||||
*/
|
||||
int commits;
|
||||
int trees;
|
||||
int blobs;
|
||||
int tags;
|
||||
|
||||
/**
|
||||
* When 'prune_all_uninteresting' is set and a path has all objects
|
||||
* marked as UNINTERESTING, then the path-walk will not visit those
|
||||
* objects. It will not call path_fn on those objects and will not
|
||||
* walk the children of such trees.
|
||||
*/
|
||||
int prune_all_uninteresting;
|
||||
};
|
||||
|
||||
#define PATH_WALK_INFO_INIT { \
|
||||
.blobs = 1, \
|
||||
.trees = 1, \
|
||||
.commits = 1, \
|
||||
.tags = 1, \
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the configuration of 'info', walk the commits based on 'info->revs' and
|
||||
* call 'info->path_fn' on each discovered path.
|
||||
*
|
||||
* Returns nonzero on an error.
|
||||
*/
|
||||
int walk_objects_by_path(struct path_walk_info *info);
|
||||
@ -45,11 +45,13 @@ void prepare_repo_settings(struct repository *r)
|
||||
r->settings.fetch_negotiation_algorithm = FETCH_NEGOTIATION_SKIPPING;
|
||||
r->settings.pack_use_bitmap_boundary_traversal = 1;
|
||||
r->settings.pack_use_multi_pack_reuse = 1;
|
||||
r->settings.pack_use_path_walk = 1;
|
||||
}
|
||||
if (manyfiles) {
|
||||
r->settings.index_version = 4;
|
||||
r->settings.index_skip_hash = 1;
|
||||
r->settings.core_untracked_cache = UNTRACKED_CACHE_WRITE;
|
||||
r->settings.pack_use_path_walk = 1;
|
||||
}
|
||||
|
||||
/* Commit graph config or default, does not cascade (simple) */
|
||||
@ -64,6 +66,7 @@ void prepare_repo_settings(struct repository *r)
|
||||
|
||||
/* Boolean config or default, does not cascade (simple) */
|
||||
repo_cfg_bool(r, "pack.usesparse", &r->settings.pack_use_sparse, 1);
|
||||
repo_cfg_bool(r, "pack.usepathwalk", &r->settings.pack_use_path_walk, 0);
|
||||
repo_cfg_bool(r, "core.multipackindex", &r->settings.core_multi_pack_index, 1);
|
||||
repo_cfg_bool(r, "index.sparse", &r->settings.sparse_index, 0);
|
||||
repo_cfg_bool(r, "index.skiphash", &r->settings.index_skip_hash, r->settings.index_skip_hash);
|
||||
|
||||
@ -53,6 +53,7 @@ struct repo_settings {
|
||||
enum untracked_cache_setting core_untracked_cache;
|
||||
|
||||
int pack_use_sparse;
|
||||
int pack_use_path_walk;
|
||||
enum fetch_negotiation_setting fetch_negotiation_algorithm;
|
||||
|
||||
int core_multi_pack_index;
|
||||
|
||||
15
revision.c
15
revision.c
@ -219,6 +219,21 @@ static void add_children_by_path(struct repository *r,
|
||||
free_tree_buffer(tree);
|
||||
}
|
||||
|
||||
void mark_trees_uninteresting_dense(struct repository *r,
|
||||
struct oidset *trees)
|
||||
{
|
||||
struct object_id *oid;
|
||||
struct oidset_iter iter;
|
||||
|
||||
oidset_iter_init(trees, &iter);
|
||||
while ((oid = oidset_iter_next(&iter))) {
|
||||
struct tree *tree = lookup_tree(r, oid);
|
||||
|
||||
if (tree->object.flags & UNINTERESTING)
|
||||
mark_tree_contents_uninteresting(r, tree);
|
||||
}
|
||||
}
|
||||
|
||||
void mark_trees_uninteresting_sparse(struct repository *r,
|
||||
struct oidset *trees)
|
||||
{
|
||||
|
||||
@ -487,6 +487,7 @@ void put_revision_mark(const struct rev_info *revs,
|
||||
|
||||
void mark_parents_uninteresting(struct rev_info *revs, struct commit *commit);
|
||||
void mark_tree_uninteresting(struct repository *r, struct tree *tree);
|
||||
void mark_trees_uninteresting_dense(struct repository *r, struct oidset *trees);
|
||||
void mark_trees_uninteresting_sparse(struct repository *r, struct oidset *trees);
|
||||
|
||||
void show_object_with_name(FILE *, struct object *, const char *);
|
||||
|
||||
1
scalar.c
1
scalar.c
@ -170,6 +170,7 @@ static int set_recommended_config(int reconfigure)
|
||||
{ "core.autoCRLF", "false" },
|
||||
{ "core.safeCRLF", "false" },
|
||||
{ "fetch.showForcedUpdates", "false" },
|
||||
{ "pack.usePathWalk", "true" },
|
||||
{ NULL, NULL },
|
||||
};
|
||||
int i;
|
||||
|
||||
4
t/README
4
t/README
@ -436,6 +436,10 @@ GIT_TEST_PACK_SPARSE=<boolean> if disabled will default the pack-objects
|
||||
builtin to use the non-sparse object walk. This can still be overridden by
|
||||
the --sparse command-line argument.
|
||||
|
||||
GIT_TEST_PACK_PATH_WALK=<boolean> if enabled will default the pack-objects
|
||||
builtin to use the path-walk API for the object walk. This can still be
|
||||
overridden by the --no-path-walk command-line argument.
|
||||
|
||||
GIT_TEST_PRELOAD_INDEX=<boolean> exercises the preload-index code path
|
||||
by overriding the minimum number of cache entries required per thread.
|
||||
|
||||
|
||||
114
t/helper/test-path-walk.c
Normal file
114
t/helper/test-path-walk.c
Normal file
@ -0,0 +1,114 @@
|
||||
#define USE_THE_REPOSITORY_VARIABLE
|
||||
|
||||
#include "test-tool.h"
|
||||
#include "environment.h"
|
||||
#include "hex.h"
|
||||
#include "object-name.h"
|
||||
#include "object.h"
|
||||
#include "pretty.h"
|
||||
#include "revision.h"
|
||||
#include "setup.h"
|
||||
#include "parse-options.h"
|
||||
#include "path-walk.h"
|
||||
#include "oid-array.h"
|
||||
|
||||
static const char * const path_walk_usage[] = {
|
||||
N_("test-tool path-walk <options> -- <revision-options>"),
|
||||
NULL
|
||||
};
|
||||
|
||||
struct path_walk_test_data {
|
||||
uintmax_t commit_nr;
|
||||
uintmax_t tree_nr;
|
||||
uintmax_t blob_nr;
|
||||
uintmax_t tag_nr;
|
||||
};
|
||||
|
||||
static int emit_block(const char *path, struct oid_array *oids,
|
||||
enum object_type type, void *data)
|
||||
{
|
||||
struct path_walk_test_data *tdata = data;
|
||||
const char *typestr;
|
||||
|
||||
switch (type) {
|
||||
case OBJ_COMMIT:
|
||||
typestr = "COMMIT";
|
||||
tdata->commit_nr += oids->nr;
|
||||
break;
|
||||
|
||||
case OBJ_TREE:
|
||||
typestr = "TREE";
|
||||
tdata->tree_nr += oids->nr;
|
||||
break;
|
||||
|
||||
case OBJ_BLOB:
|
||||
typestr = "BLOB";
|
||||
tdata->blob_nr += oids->nr;
|
||||
break;
|
||||
|
||||
case OBJ_TAG:
|
||||
typestr = "TAG";
|
||||
tdata->tag_nr += oids->nr;
|
||||
break;
|
||||
|
||||
default:
|
||||
BUG("we do not understand this type");
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < oids->nr; i++) {
|
||||
struct object *o = lookup_unknown_object(the_repository,
|
||||
&oids->oid[i]);
|
||||
printf("%s:%s:%s%s\n", typestr, path, oid_to_hex(&oids->oid[i]),
|
||||
o->flags & UNINTERESTING ? ":UNINTERESTING" : "");
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cmd__path_walk(int argc, const char **argv)
|
||||
{
|
||||
int res;
|
||||
struct rev_info revs = REV_INFO_INIT;
|
||||
struct path_walk_info info = PATH_WALK_INFO_INIT;
|
||||
struct path_walk_test_data data = { 0 };
|
||||
struct option options[] = {
|
||||
OPT_BOOL(0, "blobs", &info.blobs,
|
||||
N_("toggle inclusion of blob objects")),
|
||||
OPT_BOOL(0, "commits", &info.commits,
|
||||
N_("toggle inclusion of commit objects")),
|
||||
OPT_BOOL(0, "tags", &info.tags,
|
||||
N_("toggle inclusion of tag objects")),
|
||||
OPT_BOOL(0, "trees", &info.trees,
|
||||
N_("toggle inclusion of tree objects")),
|
||||
OPT_BOOL(0, "prune", &info.prune_all_uninteresting,
|
||||
N_("toggle pruning of uninteresting paths")),
|
||||
OPT_END(),
|
||||
};
|
||||
|
||||
initialize_repository(the_repository);
|
||||
setup_git_directory();
|
||||
revs.repo = the_repository;
|
||||
|
||||
argc = parse_options(argc, argv, NULL,
|
||||
options, path_walk_usage,
|
||||
PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0);
|
||||
|
||||
if (argc > 1)
|
||||
setup_revisions(argc, argv, &revs, NULL);
|
||||
else
|
||||
usage(path_walk_usage[0]);
|
||||
|
||||
info.revs = &revs;
|
||||
info.path_fn = emit_block;
|
||||
info.path_fn_data = &data;
|
||||
|
||||
res = walk_objects_by_path(&info);
|
||||
|
||||
printf("commits:%" PRIuMAX "\n"
|
||||
"trees:%" PRIuMAX "\n"
|
||||
"blobs:%" PRIuMAX "\n"
|
||||
"tags:%" PRIuMAX "\n",
|
||||
data.commit_nr, data.tree_nr, data.blob_nr, data.tag_nr);
|
||||
|
||||
return res;
|
||||
}
|
||||
@ -53,6 +53,7 @@ static struct test_cmd cmds[] = {
|
||||
{ "parse-subcommand", cmd__parse_subcommand },
|
||||
{ "partial-clone", cmd__partial_clone },
|
||||
{ "path-utils", cmd__path_utils },
|
||||
{ "path-walk", cmd__path_walk },
|
||||
{ "pcre2-config", cmd__pcre2_config },
|
||||
{ "pkt-line", cmd__pkt_line },
|
||||
{ "proc-receive", cmd__proc_receive },
|
||||
|
||||
@ -46,6 +46,7 @@ int cmd__parse_pathspec_file(int argc, const char** argv);
|
||||
int cmd__parse_subcommand(int argc, const char **argv);
|
||||
int cmd__partial_clone(int argc, const char **argv);
|
||||
int cmd__path_utils(int argc, const char **argv);
|
||||
int cmd__path_walk(int argc, const char **argv);
|
||||
int cmd__pcre2_config(int argc, const char **argv);
|
||||
int cmd__pkt_line(int argc, const char **argv);
|
||||
int cmd__proc_receive(int argc, const char **argv);
|
||||
|
||||
@ -36,6 +36,14 @@ test_size 'thin pack size with --full-name-hash' '
|
||||
test_file_size out
|
||||
'
|
||||
|
||||
test_perf 'thin pack with --path-walk' '
|
||||
git pack-objects --thin --stdout --revs --sparse --path-walk <in-thin >out
|
||||
'
|
||||
|
||||
test_size 'thin pack size with --path-walk' '
|
||||
test_file_size out
|
||||
'
|
||||
|
||||
test_perf 'big pack' '
|
||||
git pack-objects --stdout --revs --sparse <in-big >out
|
||||
'
|
||||
@ -52,6 +60,14 @@ test_size 'big pack size with --full-name-hash' '
|
||||
test_file_size out
|
||||
'
|
||||
|
||||
test_perf 'big pack with --path-walk' '
|
||||
git pack-objects --stdout --revs --sparse --path-walk <in-big >out
|
||||
'
|
||||
|
||||
test_size 'big pack size with --path-walk' '
|
||||
test_file_size out
|
||||
'
|
||||
|
||||
test_perf 'repack' '
|
||||
git repack -adf
|
||||
'
|
||||
@ -70,4 +86,13 @@ test_size 'repack size with --full-name-hash' '
|
||||
test_file_size "$pack"
|
||||
'
|
||||
|
||||
test_perf 'repack with --path-walk' '
|
||||
git repack -adf --path-walk
|
||||
'
|
||||
|
||||
test_size 'repack size with --path-walk' '
|
||||
pack=$(ls .git/objects/pack/pack-*.pack) &&
|
||||
test_file_size "$pack"
|
||||
'
|
||||
|
||||
test_done
|
||||
|
||||
@ -63,6 +63,12 @@ test_expect_success 'pack-objects should fetch from promisor remote and execute
|
||||
|
||||
test_expect_success 'clone from promisor remote does not lazy-fetch by default' '
|
||||
rm -f script-executed &&
|
||||
|
||||
# The --path-walk feature of "git pack-objects" is not
|
||||
# compatible with this kind of fetch from an incomplete repo.
|
||||
GIT_TEST_PACK_PATH_WALK=0 &&
|
||||
export GIT_TEST_PACK_PATH_WALK &&
|
||||
|
||||
test_must_fail git clone evil no-lazy 2>err &&
|
||||
test_grep "lazy fetching disabled" err &&
|
||||
test_path_is_missing script-executed
|
||||
|
||||
@ -689,4 +689,25 @@ test_expect_success '--full-name-hash and --write-bitmap-index are incompatible'
|
||||
git pack-objects --stdout --all --full-name-hash --write-bitmap-index >out
|
||||
'
|
||||
|
||||
# Basic "repack everything" test
|
||||
test_expect_success '--path-walk pack everything' '
|
||||
git -C server rev-parse HEAD >in &&
|
||||
GIT_PROGRESS_DELAY=0 git -C server pack-objects \
|
||||
--stdout --revs --path-walk --progress <in >out.pack 2>err &&
|
||||
grep "Compressing objects by path" err &&
|
||||
git -C server index-pack --stdin <out.pack
|
||||
'
|
||||
|
||||
# Basic "thin pack" test
|
||||
test_expect_success '--path-walk thin pack' '
|
||||
cat >in <<-EOF &&
|
||||
$(git -C server rev-parse HEAD)
|
||||
^$(git -C server rev-parse HEAD~2)
|
||||
EOF
|
||||
GIT_PROGRESS_DELAY=0 git -C server pack-objects \
|
||||
--thin --stdout --revs --path-walk --progress <in >out.pack 2>err &&
|
||||
grep "Compressing objects by path" err &&
|
||||
git -C server index-pack --fix-thin --stdin <out.pack
|
||||
'
|
||||
|
||||
test_done
|
||||
|
||||
@ -60,6 +60,11 @@ test_expect_success 'indirectly clone patch_clone' '
|
||||
git pull ../.git &&
|
||||
test $(git rev-parse HEAD) = $B &&
|
||||
|
||||
# The --path-walk feature of "git pack-objects" is not
|
||||
# compatible with this kind of fetch from an incomplete repo.
|
||||
GIT_TEST_PACK_PATH_WALK=0 &&
|
||||
export GIT_TEST_PACK_PATH_WALK &&
|
||||
|
||||
git pull ../patch_clone/.git &&
|
||||
test $(git rev-parse HEAD) = $C
|
||||
)
|
||||
|
||||
@ -155,8 +155,9 @@ test_bitmap_cases () {
|
||||
ls .git/objects/pack/ | grep bitmap >output &&
|
||||
test_line_count = 1 output &&
|
||||
# verify equivalent packs are generated with/without using bitmap index
|
||||
packasha1=$(git pack-objects --no-use-bitmap-index --all packa </dev/null) &&
|
||||
packbsha1=$(git pack-objects --use-bitmap-index --all packb </dev/null) &&
|
||||
# Be careful to not use the path-walk option in either case.
|
||||
packasha1=$(git pack-objects --no-use-bitmap-index --no-path-walk --all packa </dev/null) &&
|
||||
packbsha1=$(git pack-objects --use-bitmap-index --no-path-walk --all packb </dev/null) &&
|
||||
list_packed_objects packa-$packasha1.idx >packa.objects &&
|
||||
list_packed_objects packb-$packbsha1.idx >packb.objects &&
|
||||
test_cmp packa.objects packb.objects
|
||||
@ -385,6 +386,14 @@ test_bitmap_cases () {
|
||||
git init --bare client.git &&
|
||||
(
|
||||
cd client.git &&
|
||||
|
||||
# This test relies on reusing a delta, but if the
|
||||
# path-walk machinery is engaged, the base object
|
||||
# is considered too small to use during the
|
||||
# dynamic computation, so is not used.
|
||||
GIT_TEST_PACK_PATH_WALK=0 &&
|
||||
export GIT_TEST_PACK_PATH_WALK &&
|
||||
|
||||
git config transfer.unpackLimit 1 &&
|
||||
git fetch .. delta-reuse-old:delta-reuse-old &&
|
||||
git fetch .. delta-reuse-new:delta-reuse-new &&
|
||||
|
||||
@ -90,15 +90,18 @@ max_chain() {
|
||||
# adjusted (or scrapped if the heuristics have become too unreliable)
|
||||
test_expect_success 'packing produces a long delta' '
|
||||
# Use --window=0 to make sure we are seeing reused deltas,
|
||||
# not computing a new long chain.
|
||||
pack=$(git pack-objects --all --window=0 </dev/null pack) &&
|
||||
# not computing a new long chain. (Also avoid the --path-walk
|
||||
# option as it may break delta chains.)
|
||||
pack=$(git pack-objects --all --window=0 --no-path-walk </dev/null pack) &&
|
||||
echo 9 >expect &&
|
||||
max_chain pack-$pack.pack >actual &&
|
||||
test_cmp expect actual
|
||||
'
|
||||
|
||||
test_expect_success '--depth limits depth' '
|
||||
pack=$(git pack-objects --all --depth=5 </dev/null pack) &&
|
||||
# Avoid --path-walk to avoid breaking delta chains across path
|
||||
# boundaries.
|
||||
pack=$(git pack-objects --all --depth=5 --no-path-walk </dev/null pack) &&
|
||||
echo 5 >expect &&
|
||||
max_chain pack-$pack.pack >actual &&
|
||||
test_cmp expect actual
|
||||
|
||||
@ -8,6 +8,13 @@ TEST_PASSES_SANITIZE_LEAK=true
|
||||
|
||||
GIT_TEST_MULTI_PACK_INDEX=0
|
||||
GIT_TEST_MULTI_PACK_INDEX_WRITE_INCREMENTAL=0
|
||||
|
||||
# The --path-walk option does not consider the preferred pack
|
||||
# at all for reusing deltas, so this variable changes the
|
||||
# behavior of this test, if enabled.
|
||||
GIT_TEST_PACK_PATH_WALK=0
|
||||
export GIT_TEST_PACK_PATH_WALK
|
||||
|
||||
objdir=.git/objects
|
||||
packdir=$objdir/pack
|
||||
|
||||
|
||||
279
t/t6601-path-walk.sh
Executable file
279
t/t6601-path-walk.sh
Executable file
@ -0,0 +1,279 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='direct path-walk API tests'
|
||||
|
||||
. ./test-lib.sh
|
||||
|
||||
test_expect_success 'setup test repository' '
|
||||
git checkout -b base &&
|
||||
|
||||
# Make some objects that will only be reachable
|
||||
# via non-commit tags.
|
||||
mkdir child &&
|
||||
echo file >child/file &&
|
||||
git add child &&
|
||||
git commit -m "will abandon" &&
|
||||
git tag -a -m "tree" tree-tag HEAD^{tree} &&
|
||||
echo file2 >file2 &&
|
||||
git add file2 &&
|
||||
git commit --amend -m "will abandon" &&
|
||||
git tag tree-tag2 HEAD^{tree} &&
|
||||
|
||||
echo blob >file &&
|
||||
blob_oid=$(git hash-object -t blob -w --stdin <file) &&
|
||||
git tag -a -m "blob" blob-tag "$blob_oid" &&
|
||||
echo blob2 >file2 &&
|
||||
blob2_oid=$(git hash-object -t blob -w --stdin <file2) &&
|
||||
git tag blob-tag2 "$blob2_oid" &&
|
||||
|
||||
rm -fr child file file2 &&
|
||||
|
||||
mkdir left &&
|
||||
mkdir right &&
|
||||
echo a >a &&
|
||||
echo b >left/b &&
|
||||
echo c >right/c &&
|
||||
git add . &&
|
||||
git commit --amend -m "first" &&
|
||||
git tag -m "first" first HEAD &&
|
||||
|
||||
echo d >right/d &&
|
||||
git add right &&
|
||||
git commit -m "second" &&
|
||||
git tag -a -m "second (under)" second.1 HEAD &&
|
||||
git tag -a -m "second (top)" second.2 second.1 &&
|
||||
|
||||
# Set up file/dir collision in history.
|
||||
rm a &&
|
||||
mkdir a &&
|
||||
echo a >a/a &&
|
||||
echo bb >left/b &&
|
||||
git add a left &&
|
||||
git commit -m "third" &&
|
||||
git tag -a -m "third" third &&
|
||||
|
||||
git checkout -b topic HEAD~1 &&
|
||||
echo cc >right/c &&
|
||||
git commit -a -m "topic" &&
|
||||
git tag -a -m "fourth" fourth
|
||||
'
|
||||
|
||||
test_expect_success 'all' '
|
||||
test-tool path-walk -- --all >out &&
|
||||
|
||||
cat >expect <<-EOF &&
|
||||
COMMIT::$(git rev-parse topic)
|
||||
COMMIT::$(git rev-parse base)
|
||||
COMMIT::$(git rev-parse base~1)
|
||||
COMMIT::$(git rev-parse base~2)
|
||||
commits:4
|
||||
TREE::$(git rev-parse topic^{tree})
|
||||
TREE::$(git rev-parse base^{tree})
|
||||
TREE::$(git rev-parse base~1^{tree})
|
||||
TREE::$(git rev-parse base~2^{tree})
|
||||
TREE::$(git rev-parse refs/tags/tree-tag^{})
|
||||
TREE::$(git rev-parse refs/tags/tree-tag2^{})
|
||||
TREE:a/:$(git rev-parse base:a)
|
||||
TREE:left/:$(git rev-parse base:left)
|
||||
TREE:left/:$(git rev-parse base~2:left)
|
||||
TREE:right/:$(git rev-parse topic:right)
|
||||
TREE:right/:$(git rev-parse base~1:right)
|
||||
TREE:right/:$(git rev-parse base~2:right)
|
||||
TREE:child/:$(git rev-parse refs/tags/tree-tag^{}:child)
|
||||
trees:13
|
||||
BLOB:a:$(git rev-parse base~2:a)
|
||||
BLOB:file2:$(git rev-parse refs/tags/tree-tag2^{}:file2)
|
||||
BLOB:left/b:$(git rev-parse base~2:left/b)
|
||||
BLOB:left/b:$(git rev-parse base:left/b)
|
||||
BLOB:right/c:$(git rev-parse base~2:right/c)
|
||||
BLOB:right/c:$(git rev-parse topic:right/c)
|
||||
BLOB:right/d:$(git rev-parse base~1:right/d)
|
||||
BLOB:/tagged-blobs:$(git rev-parse refs/tags/blob-tag^{})
|
||||
BLOB:/tagged-blobs:$(git rev-parse refs/tags/blob-tag2^{})
|
||||
BLOB:child/file:$(git rev-parse refs/tags/tree-tag^{}:child/file)
|
||||
blobs:10
|
||||
TAG::$(git rev-parse refs/tags/first)
|
||||
TAG::$(git rev-parse refs/tags/second.1)
|
||||
TAG::$(git rev-parse refs/tags/second.2)
|
||||
TAG::$(git rev-parse refs/tags/third)
|
||||
TAG::$(git rev-parse refs/tags/fourth)
|
||||
TAG::$(git rev-parse refs/tags/tree-tag)
|
||||
TAG::$(git rev-parse refs/tags/blob-tag)
|
||||
tags:7
|
||||
EOF
|
||||
|
||||
sort expect >expect.sorted &&
|
||||
sort out >out.sorted &&
|
||||
|
||||
test_cmp expect.sorted out.sorted
|
||||
'
|
||||
|
||||
test_expect_success 'topic only' '
|
||||
test-tool path-walk -- topic >out &&
|
||||
|
||||
cat >expect <<-EOF &&
|
||||
COMMIT::$(git rev-parse topic)
|
||||
COMMIT::$(git rev-parse base~1)
|
||||
COMMIT::$(git rev-parse base~2)
|
||||
commits:3
|
||||
TREE::$(git rev-parse topic^{tree})
|
||||
TREE::$(git rev-parse base~1^{tree})
|
||||
TREE::$(git rev-parse base~2^{tree})
|
||||
TREE:left/:$(git rev-parse base~2:left)
|
||||
TREE:right/:$(git rev-parse topic:right)
|
||||
TREE:right/:$(git rev-parse base~1:right)
|
||||
TREE:right/:$(git rev-parse base~2:right)
|
||||
trees:7
|
||||
BLOB:a:$(git rev-parse base~2:a)
|
||||
BLOB:left/b:$(git rev-parse base~2:left/b)
|
||||
BLOB:right/c:$(git rev-parse base~2:right/c)
|
||||
BLOB:right/c:$(git rev-parse topic:right/c)
|
||||
BLOB:right/d:$(git rev-parse base~1:right/d)
|
||||
blobs:5
|
||||
tags:0
|
||||
EOF
|
||||
|
||||
sort expect >expect.sorted &&
|
||||
sort out >out.sorted &&
|
||||
|
||||
test_cmp expect.sorted out.sorted
|
||||
'
|
||||
|
||||
test_expect_success 'topic, not base' '
|
||||
test-tool path-walk -- topic --not base >out &&
|
||||
|
||||
cat >expect <<-EOF &&
|
||||
COMMIT::$(git rev-parse topic)
|
||||
commits:1
|
||||
TREE::$(git rev-parse topic^{tree})
|
||||
TREE:left/:$(git rev-parse topic:left)
|
||||
TREE:right/:$(git rev-parse topic:right)
|
||||
trees:3
|
||||
BLOB:a:$(git rev-parse topic:a)
|
||||
BLOB:left/b:$(git rev-parse topic:left/b)
|
||||
BLOB:right/c:$(git rev-parse topic:right/c)
|
||||
BLOB:right/d:$(git rev-parse topic:right/d)
|
||||
blobs:4
|
||||
tags:0
|
||||
EOF
|
||||
|
||||
sort expect >expect.sorted &&
|
||||
sort out >out.sorted &&
|
||||
|
||||
test_cmp expect.sorted out.sorted
|
||||
'
|
||||
|
||||
test_expect_success 'topic, not base, only blobs' '
|
||||
test-tool path-walk --no-trees --no-commits \
|
||||
-- topic --not base >out &&
|
||||
|
||||
cat >expect <<-EOF &&
|
||||
commits:0
|
||||
trees:0
|
||||
BLOB:a:$(git rev-parse topic:a)
|
||||
BLOB:left/b:$(git rev-parse topic:left/b)
|
||||
BLOB:right/c:$(git rev-parse topic:right/c)
|
||||
BLOB:right/d:$(git rev-parse topic:right/d)
|
||||
blobs:4
|
||||
tags:0
|
||||
EOF
|
||||
|
||||
sort expect >expect.sorted &&
|
||||
sort out >out.sorted &&
|
||||
|
||||
test_cmp expect.sorted out.sorted
|
||||
'
|
||||
|
||||
# No, this doesn't make a lot of sense for the path-walk API,
|
||||
# but it is possible to do.
|
||||
test_expect_success 'topic, not base, only commits' '
|
||||
test-tool path-walk --no-blobs --no-trees \
|
||||
-- topic --not base >out &&
|
||||
|
||||
cat >expect <<-EOF &&
|
||||
COMMIT::$(git rev-parse topic)
|
||||
commits:1
|
||||
trees:0
|
||||
blobs:0
|
||||
tags:0
|
||||
EOF
|
||||
|
||||
sort expect >expect.sorted &&
|
||||
sort out >out.sorted &&
|
||||
|
||||
test_cmp expect.sorted out.sorted
|
||||
'
|
||||
|
||||
test_expect_success 'topic, not base, only trees' '
|
||||
test-tool path-walk --no-blobs --no-commits \
|
||||
-- topic --not base >out &&
|
||||
|
||||
cat >expect <<-EOF &&
|
||||
commits:0
|
||||
TREE::$(git rev-parse topic^{tree})
|
||||
TREE:left/:$(git rev-parse topic:left)
|
||||
TREE:right/:$(git rev-parse topic:right)
|
||||
trees:3
|
||||
blobs:0
|
||||
tags:0
|
||||
EOF
|
||||
|
||||
sort expect >expect.sorted &&
|
||||
sort out >out.sorted &&
|
||||
|
||||
test_cmp expect.sorted out.sorted
|
||||
'
|
||||
|
||||
test_expect_success 'topic, not base, boundary' '
|
||||
test-tool path-walk -- --boundary topic --not base >out &&
|
||||
|
||||
cat >expect <<-EOF &&
|
||||
COMMIT::$(git rev-parse topic)
|
||||
COMMIT::$(git rev-parse base~1):UNINTERESTING
|
||||
commits:2
|
||||
TREE::$(git rev-parse topic^{tree})
|
||||
TREE::$(git rev-parse base~1^{tree}):UNINTERESTING
|
||||
TREE:left/:$(git rev-parse base~1:left):UNINTERESTING
|
||||
TREE:right/:$(git rev-parse topic:right)
|
||||
TREE:right/:$(git rev-parse base~1:right):UNINTERESTING
|
||||
trees:5
|
||||
BLOB:a:$(git rev-parse base~1:a):UNINTERESTING
|
||||
BLOB:left/b:$(git rev-parse base~1:left/b):UNINTERESTING
|
||||
BLOB:right/c:$(git rev-parse base~1:right/c):UNINTERESTING
|
||||
BLOB:right/c:$(git rev-parse topic:right/c)
|
||||
BLOB:right/d:$(git rev-parse base~1:right/d):UNINTERESTING
|
||||
blobs:5
|
||||
tags:0
|
||||
EOF
|
||||
|
||||
sort expect >expect.sorted &&
|
||||
sort out >out.sorted &&
|
||||
|
||||
test_cmp expect.sorted out.sorted
|
||||
'
|
||||
|
||||
test_expect_success 'topic, not base, boundary with pruning' '
|
||||
test-tool path-walk --prune -- --boundary topic --not base >out &&
|
||||
|
||||
cat >expect <<-EOF &&
|
||||
COMMIT::$(git rev-parse topic)
|
||||
COMMIT::$(git rev-parse base~1):UNINTERESTING
|
||||
commits:2
|
||||
TREE::$(git rev-parse topic^{tree})
|
||||
TREE::$(git rev-parse base~1^{tree}):UNINTERESTING
|
||||
TREE:right/:$(git rev-parse topic:right)
|
||||
TREE:right/:$(git rev-parse base~1:right):UNINTERESTING
|
||||
trees:4
|
||||
BLOB:right/c:$(git rev-parse base~1:right/c):UNINTERESTING
|
||||
BLOB:right/c:$(git rev-parse topic:right/c)
|
||||
blobs:2
|
||||
tags:0
|
||||
EOF
|
||||
|
||||
sort expect >expect.sorted &&
|
||||
sort out >out.sorted &&
|
||||
|
||||
test_cmp expect.sorted out.sorted
|
||||
'
|
||||
|
||||
test_done
|
||||
@ -1103,12 +1103,16 @@ test_expect_success 'submodule update --quiet passes quietness to fetch with a s
|
||||
) &&
|
||||
git clone super4 super5 &&
|
||||
(cd super5 &&
|
||||
# This test variable will create a "warning" message to stderr
|
||||
GIT_TEST_PACK_PATH_WALK=0 \
|
||||
git submodule update --quiet --init --depth=1 submodule3 >out 2>err &&
|
||||
test_must_be_empty out &&
|
||||
test_must_be_empty err
|
||||
) &&
|
||||
git clone super4 super6 &&
|
||||
(cd super6 &&
|
||||
# This test variable will create a "warning" message to stderr
|
||||
GIT_TEST_PACK_PATH_WALK=0 \
|
||||
git submodule update --init --depth=1 submodule3 >out 2>err &&
|
||||
test_file_not_empty out &&
|
||||
test_file_not_empty err
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user