diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 205325eb33b064..4fff34487453d9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,18 +5,20 @@ on: [push, pull_request] env: DEVELOPER: 1 -# If more than one workflow run is triggered for the very same commit hash -# (which happens when multiple branches pointing to the same commit), only -# the first one is allowed to run, the second will be kept in the "queued" -# state. This allows a successful completion of the first run to be reused -# in the second run via the `skip-if-redundant` logic in the `config` job. +# For pull requests, only the latest workflow run is allowed to proceed. +# Older runs are canceled when a new revision is pushed. # -# The only caveat is that if a workflow run is triggered for the same commit -# hash that another run is already being held, that latter run will be -# canceled. For more details about the `concurrency` attribute, see: +# For pushes, if more than one workflow run is triggered for the very same +# commit hash (which happens when multiple branches point to the same commit), +# only the first one is allowed to run. This allows a successful completion of +# the first run to be reused in the second run via the `skip-if-redundant` +# logic in the `config` job. +# +# For more details about the `concurrency` attribute, see: # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency concurrency: - group: ${{ github.sha }} + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: ci-config: diff --git a/Documentation/Makefile b/Documentation/Makefile index 2699f0b24af192..f8dea4b3953250 100644 --- a/Documentation/Makefile +++ b/Documentation/Makefile @@ -129,6 +129,7 @@ TECH_DOCS += technical/long-running-process-protocol TECH_DOCS += technical/multi-pack-index TECH_DOCS += technical/packfile-uri TECH_DOCS += technical/pack-heuristics +TECH_DOCS += technical/paint-down-to-common TECH_DOCS += technical/parallel-checkout TECH_DOCS += technical/partial-clone TECH_DOCS += technical/platform-support diff --git a/Documentation/SubmittingPatches b/Documentation/SubmittingPatches index c269e474e3f2bf..c60855f7069492 100644 --- a/Documentation/SubmittingPatches +++ b/Documentation/SubmittingPatches @@ -117,7 +117,7 @@ of review. can still continue to further improve them by adding more patches on top, but by the time a topic gets merged to 'next', it is expected that everybody agrees that the scope and the basic direction of the - topic are appropriate, so such an incremental updates are limited to + topic are appropriate, so such incremental updates are limited to small corrections and polishing. After a topic cooks for some time (like 7 calendar days) in 'next' without needing further tweaks on top, it gets merged to the 'master' branch and waits to become part diff --git a/Documentation/config/branch.adoc b/Documentation/config/branch.adoc index 5a85fde8de9286..c2acd89c9936cb 100644 --- a/Documentation/config/branch.adoc +++ b/Documentation/config/branch.adoc @@ -103,3 +103,10 @@ for details). `git branch --edit-description`. Branch description is automatically added to the `format-patch` cover letter or `request-pull` summary. + +`branch..deleteMerged`:: + If set to `false`, branch __ is exempt from + `git branch --delete-merged`. Useful for a topic branch you + intend to develop further after an initial round has been + merged upstream. Defaults to true. Explicit deletion via + `git branch -d` is unaffected. diff --git a/Documentation/git-bisect.adoc b/Documentation/git-bisect.adoc index d2115b29905f41..aabddd42ca4d31 100644 --- a/Documentation/git-bisect.adoc +++ b/Documentation/git-bisect.adoc @@ -10,7 +10,7 @@ SYNOPSIS -------- [synopsis] git bisect start [--term-(bad|new)= --term-(good|old)=] - [--no-checkout] [--first-parent] [ [...]] [--] [...] + [--no-checkout] [--first-parent] [--reset-when-found[=]] [ [...]] [--] [...] git bisect (bad|new|) [] git bisect (good|old|) [...] git bisect terms [--term-(good|old) | --term-(bad|new)] @@ -20,7 +20,7 @@ git bisect reset [] git bisect (visualize|view) git bisect replay git bisect log -git bisect run [...] +git bisect run [--reset-when-found[=]] [...] git bisect help DESCRIPTION @@ -385,6 +385,16 @@ ignored. This option is particularly useful in avoiding false positives when a merged branch contained broken or non-buildable commits, but the merge itself was OK. +`--reset-when-found[=]`:: + Once the first bad commit is found, report it and clean up the + bisection state. `` may be `original` to return to the commit + checked out before `git bisect start`, or `found` to leave the first + bad commit checked out. If `` is omitted, it defaults to + `original`. ++ +This option may be given to `git bisect start` or to `git bisect run`. It +cannot be used for a bisection started with `--no-checkout`. + EXAMPLES -------- diff --git a/Documentation/git-branch.adoc b/Documentation/git-branch.adoc index c0afddc424d610..bfdf4593298631 100644 --- a/Documentation/git-branch.adoc +++ b/Documentation/git-branch.adoc @@ -13,6 +13,7 @@ git branch [--color[=] | --no-color] [--show-current] [--column[=] | --no-column] [--sort=] [--merged []] [--no-merged []] [--contains []] [--no-contains []] + [(--forked )...] [--points-at ] [--format=] [(-r|--remotes) | (-a|--all)] [--list] [...] @@ -24,6 +25,7 @@ git branch (-m|-M) [] git branch (-c|-C) [] git branch (-d|-D) [-r] ... git branch --edit-description [] +git branch [--dry-run] (--delete-merged )... [...] DESCRIPTION ----------- @@ -51,7 +53,8 @@ merged into the named commit (i.e. the branches whose tip commits are reachable from the named commit) will be listed. With `--no-merged` only branches not merged into the named commit will be listed. If the __ argument is missing it defaults to `HEAD` (i.e. the tip of the current -branch). +branch). With `--forked`, only branches whose configured upstream matches +the given branch or pattern will be listed. The command's second form creates a new branch head named __ which points to the current `HEAD`, or __ if given. As a @@ -199,6 +202,45 @@ This option is only applicable in non-verbose mode. Print the name of the current branch. In detached `HEAD` state, nothing is printed. +`--delete-merged `:: + Delete local branches whose configured upstream matches + __, but only when their tip is reachable from that + upstream. In other words, the work on the branch has already + landed on the upstream it tracks, so the local copy is no longer + needed. __ may name a ref, a remote (using the branch its + `HEAD` points at), or a shell-style glob. The option can be + repeated to widen the upstream match. + Optional __ arguments limit which local branches + are considered, e.g. `git branch --delete-merged 'origin/*' + 'topic-*'`. ++ +A branch is not deleted when: ++ +-- +* its configured upstream ref no longer exists, +* it is checked out in any worktree, +* pushing it to the remote configured by + `branch..remote` would update its upstream, so it cannot be + distinguished from a branch that just looks fully merged right + after a pull; this is determined by the remote's configured push and + fetch refspecs, +* it is the local upstream of a branch that is not being deleted, or +* `branch..deleteMerged` is set to `false`. +-- ++ +When such a local upstream branch has its own upstream deleted by the +same operation, its upstream configuration is cleared. ++ +A branch whose work has not yet been merged into its upstream is +silently skipped. Delete it with `git branch -D` if you want to +remove it anyway. + +`--dry-run`:: + With `--delete-merged`, print which branches would be + deleted and exit without touching any ref. Useful for + sanity-checking a wide pattern like `'origin/*'` before + committing to the deletion. + `-v`:: `-vv`:: `--verbose`:: @@ -311,6 +353,14 @@ superproject's "origin/main", but tracks the submodule's "origin/main". Only list branches whose tips are not reachable from __ (`HEAD` if not specified). Implies `--list`. +`--forked `:: + Only list branches whose configured upstream matches + __. The argument can be a ref (e.g. `origin/main`, + `master`), a remote name like `origin` for the branch its + `origin/HEAD` points at, or a shell-style glob (e.g. + `'origin/*'`). The option can be repeated to widen the + filter. Implies `--list`. + `--points-at `:: Only list branches of __. diff --git a/Documentation/git-cat-file.adoc b/Documentation/git-cat-file.adoc index ac3b528c6f00f6..514bfc00328caf 100644 --- a/Documentation/git-cat-file.adoc +++ b/Documentation/git-cat-file.adoc @@ -348,15 +348,12 @@ newline. The available atoms are: after that first run of whitespace (i.e., the "rest" of the line) are output in place of the `%(rest)` atom. -The command `remote-object-info` only supports the `%(objectname)` and -`%(objectsize)` placeholders. See `CAVEATS` below for more information. +The command `remote-object-info` only supports the `%(objectname)`, +`%(objectsize)` and `%(objecttype)` placeholders. See `CAVEATS` below for more +information. If no format is specified, the default format is `%(objectname) -%(objecttype) %(objectsize)`, except for `remote-object-info` commands which -use `%(objectname) %(objectsize)` because `%(objecttype)` is not supported yet. - -WARNING: When "%(objecttype)" is supported, the default format WILL be unified, -so DO NOT RELY on the current default format to stay the same!!! +%(objecttype) %(objectsize)`. If `--batch` is specified, or if `--batch-command` is used with the `contents` command, the object information is followed by the object contents (consisting @@ -453,9 +450,9 @@ scripting purposes. CAVEATS ------- -Note that only `%(objectname)` and `%(objectsize)` are currently -supported by the `remote-object-info` command. Using any other placeholder in -the format string will return an empty string in its position. +Note that only `%(objectname)`, `%(objectsize)` and `%(objecttype)` are +currently supported by the `remote-object-info` command. Using any other +placeholder in the format string will return an empty string in its position. Note that the sizes of objects on disk are reported accurately, but care should be taken in drawing conclusions about which refs or objects are diff --git a/Documentation/git-fast-import.adoc b/Documentation/git-fast-import.adoc index d68bc52b7e9cd7..fd165e11d2d259 100644 --- a/Documentation/git-fast-import.adoc +++ b/Documentation/git-fast-import.adoc @@ -9,7 +9,7 @@ git-fast-import - Backend for fast Git data importers SYNOPSIS -------- [verse] -frontend | 'git fast-import' [] +'git fast-import' [] DESCRIPTION ----------- @@ -65,6 +65,13 @@ Only enable this option if you trust the program generating the fast-import stream! This option is enabled automatically for remote-helpers that use the `import` capability, as they are already trusted to run their own code. ++ +Note that this option has to be spelled in full, and has to appear +before any option whose value is separated from it by a space, for +the unsafe `feature` commands in the stream to be allowed. So +`--allow-unsafe` or `--depth 5 --allow-unsafe-features` still refuse +them, while `--allow-unsafe-features --depth 5` and +`--depth=5 --allow-unsafe-features` allow them. `--signed-tags=`:: Specify how to handle signed tags. Behaves in the same way as diff --git a/Documentation/git-interpret-trailers.adoc b/Documentation/git-interpret-trailers.adoc index 77b4f63b05cf5b..903d598dcb0595 100644 --- a/Documentation/git-interpret-trailers.adoc +++ b/Documentation/git-interpret-trailers.adoc @@ -3,7 +3,7 @@ git-interpret-trailers(1) NAME ---- -git-interpret-trailers - Add or parse structured information in commit messages +git-interpret-trailers - Add or parse metadata in commit messages SYNOPSIS -------- @@ -14,9 +14,15 @@ git interpret-trailers [--in-place] [--trim-empty] DESCRIPTION ----------- -Add or parse _trailer_ lines that look similar to RFC 822 e-mail -headers, at the end of the otherwise free-form part of a commit -message. For example, in the following commit message +Add or parse trailer metadata at the end of the otherwise +free-form part of a commit message, or any other kind of text. + +A _trailer_ in its simplest form is a key-value pair with a colon as a +separator. The _key_ consists of ASCII alphanumeric characters and +hyphens (`-`). A _trailer block_ consists of one or more trailers. The +trailer block needs to be preceded by a blank line, where a _blank line_ +is either an empty or a whitespace-only line. For example, in the +following commit message ------------------------------------------------ subject @@ -54,10 +60,18 @@ are applied to each input and the way any existing trailer in the input is changed. They also make it possible to automatically add some trailers. -By default, a `=` or `:` argument given -using `--trailer` will be appended after the existing trailers only if -the last trailer has a different (__, __) pair (or if there -is no existing trailer). The __ and __ parts will be trimmed +Let's consider new trailers added with `--trailer`. +By default, the new trailer will appear at the end of the trailer block. +Also by default, this new trailer will only be added +if the last trailer is different to it. +A trailer block will be created with only that trailer if a trailer +block does not already exist. Recall that a trailer block needs to be +preceded by a blank line, so a blank line will be inserted before the +new trailer block in that case. + +This is how the new trailer is added: a `=` or +`:` argument given using `--trailer` will be appended after +the existing trailers. The __ and __ parts will be trimmed to remove starting and trailing whitespace, and the resulting trimmed __ and __ will appear in the output like this: @@ -68,6 +82,16 @@ key: value This means that the trimmed __ and __ will be separated by "`:`{nbsp}" (one colon followed by one space). +Existing trailers are extracted from the input by looking for the +trailer block. A trailer block is a group of one or more lines that (i) +is all trailers, or (ii) contains at least one Git-generated or +user-configured trailer and consists of at +least 25% trailers. +The trailer block is by definition at the end of the commit message. +The message in turn is either (i) at the end of the input, or (ii) the +last non-whitespace lines before a line that starts with `---` (followed +by a space or the end of the line). + For convenience, a __ can be configured to make using `--trailer` shorter to type on the command line. This can be configured using the `trailer..key` configuration variable. The __ must be a prefix @@ -81,20 +105,6 @@ trailer.sign.key "Signed-off-by: " in your configuration, you only need to specify `--trailer="sign: foo"` on the command line instead of `--trailer="Signed-off-by: foo"`. -By default the new trailer will appear at the end of all the existing -trailers. If there is no existing trailer, the new trailer will appear -at the end of the input. A blank line will be added before the new -trailer if there isn't one already. - -Existing trailers are extracted from the input by looking for -a group of one or more lines that (i) is all trailers, or (ii) contains at -least one Git-generated or user-configured trailer and consists of at -least 25% trailers. -The group must be preceded by one or more empty (or whitespace-only) lines. -The group must either be at the end of the input or be the last -non-whitespace lines before a line that starts with `---` (followed by a -space or the end of the line). - When reading trailers, there can be no whitespace before or inside the __, but any number of regular space and tab characters are allowed between the __ and the separator. There can be whitespaces before, @@ -107,8 +117,22 @@ key: This is a very long value, with spaces and newlines in it. ------------------------------------------------ -Note that trailers do not follow (nor are they intended to follow) many of the -rules for RFC 822 headers. For example they do not follow the encoding rule. +OTHER RULES +----------- + +What was covered in the previous section are the rules that are relevant +for regular use. The following points are included for completeness. + +-- +* This command ignores comment lines (see `core.commentString` in + linkgit:git-config[1]). This is for use with the `prepare-commit-msg` + and `commit-msg` hooks. + +* Candidate trailer lines that have `:` as the separator, that have no + whitespace before the value part, and that start with `//` are not + recognized as trailers. This is to avoid accidentally interpreting + URLs as trailers (e.g. lines that start with `https://`). +-- OPTIONS ------- @@ -402,6 +426,29 @@ mv "\$1.new" "\$1" $ chmod +x .git/hooks/commit-msg ------------ +* Here we try to use three different trailer keys. But it fails because + two of them are not recognized as trailer keys. ++ +---- +$ cat msg.txt +subject + +Skapad-på: some-branch +Hash-in-v6.11: 45c12d3269fe48f22834320c782ffe86c3560f2c +Reviewed-by: Alice +$ git interpret-trailers --only-trailers itself rather than the reference it points to via a symbolic ref. -KNOWN LIMITATIONS ------------------ - -The ref format migration has several known limitations in its current form: - -* It is not possible to migrate repositories that have worktrees. - -* There is no way to block concurrent writes to the repository during an - ongoing migration. Concurrent writes can lead to an inconsistent migrated - state. Users are expected to block writes on a higher level. If your - repository is registered for scheduled maintenance, it is recommended to - unregister it first with git-maintenance(1). - -These limitations may eventually be lifted. - GIT --- Part of the linkgit:git[1] suite diff --git a/Documentation/git-repack.adoc b/Documentation/git-repack.adoc index 72c42015e23f94..a1f9e64f668750 100644 --- a/Documentation/git-repack.adoc +++ b/Documentation/git-repack.adoc @@ -12,6 +12,7 @@ SYNOPSIS 'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m] [--window=] [--depth=] [--threads=] [--keep-pack=] [--write-midx[=]] [--name-hash-version=] [--path-walk] + [--filter=] [--drop-filtered [--dry-run]] DESCRIPTION ----------- @@ -182,6 +183,42 @@ depth is 4095. `objects` and `objects/info/alternates` sections of linkgit:gitrepository-layout[5]. +--drop-filtered:: + Delete the local objects that match the `--filter` specification + instead of keeping them in a separate packfile, reclaiming the + disk space they occupy. This is intended for partial clones, + where the filtered objects are promisor objects that remain + recoverable from the promisor remote and are lazily re-fetched + on demand when they are next needed. ++ +Only large blobs are supported for now, so `--filter=blob:limit=` +is currently the only accepted filter. Because dropped objects must be +recoverable, this option requires a promisor remote to be configured +and refuses to run otherwise. ++ +This option requires `-a`, and implies `-d`: the objects are dropped by +rebuilding the promisor pack without them and then removing the now +redundant old packs, so the redundant packs must be deleted for the +space to actually be reclaimed. It is incompatible with `--filter-to` +and with bitmap writing (`-b`/`--write-bitmap-index`), since filtering +breaks the single-pack closure that bitmaps require. A bitmap setting +coming from configuration is silently disabled for the duration of the +command. ++ +As a convenience, since dropped objects remain recoverable by lazy fetch, +`--drop-filtered` refuses to run while another operation +(merge, rebase, am, cherry-pick, revert, or bisect) is in progress, to +avoid a surprising network fetch mid-operation, and refuses to drop any +blob that the current index references, since such a blob would only be +lazily re-fetched by the next command that inspects the working tree. +These checks are skipped in bare repositories, which have neither a +working tree nor an index. + +--dry-run:: + Only meaningful with `--drop-filtered`. List the objects that + would be dropped, one object ID per line, without rebuilding any + pack or deleting anything. + -b:: --write-bitmap-index:: Write a reachability bitmap index as part of the repack. This diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index ea4d14baddb6a9..026a2b87dc4692 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -10,7 +10,7 @@ SYNOPSIS -------- [verse] (EXPERIMENTAL!) 'git replay' ([--contained] --onto= | --advance= | --revert=) - [--ref=] [--ref-action=] + [--ref=] [--ref-action=] [--linearize] DESCRIPTION ----------- @@ -91,6 +91,23 @@ Expanded description list compared to 'replay.refAction'. + The default mode can be configured via the `replay.refAction` configuration variable. +--linearize:: + In this mode, each replayed commit is stacked on top of the + previously replayed one, so all replayed commits are flattened into + a single linear history. ++ +When a merge commit is encountered, the behavior of git-rebase(1)'s +option `--no-rebase-merges` is imitated. All commits in the range +reachable from the merge commit are replayed into a linear history, and +the merge commit itself is dropped. A ref that pointed to a merge commit +is updated to the merge's last replayed ancestor. ++ +Only a single branch can be linearized at a time: `--linearize` cannot +be combined with multiple positive revisions or with `--contained`, +because that would concatenate otherwise unrelated histories into one +line. To linearize several branches, replay them in separate `git +replay` invocations. + :: Range of commits to replay; see "Specifying Ranges" in linkgit:git-rev-parse[1]. In `--advance=` or diff --git a/Documentation/gitprotocol-v2.adoc b/Documentation/gitprotocol-v2.adoc index 7bf62014c3917d..dd52fd8110dcf1 100644 --- a/Documentation/gitprotocol-v2.adoc +++ b/Documentation/gitprotocol-v2.adoc @@ -558,14 +558,17 @@ object-info `object-info` is the command to retrieve information about one or more objects. Its main purpose is to allow a client to make decisions based on this -information without having to fully fetch objects. Object size is the only -information that is currently supported. +information without having to fully fetch objects. Currently only object size +and type are supported. An `object-info` request takes the following arguments: size Requests size information to be returned for each listed object id. + type + Requests type information to be returned for each listed object id. + oid Indicates to the server an object which the client wants to obtain information for. They must be full OIDs. @@ -580,11 +583,18 @@ space. info = *PKT-LINE(attr LF) *PKT-LINE(obj-info LF) - attr = "size" + attr = "size" | "type" obj-size = 1*DIGIT - obj-info = obj-id [SP [obj-size]] + obj-type = "blob" | "tree" | "commit" | "tag" + + obj-val = obj-size | obj-type + + obj-info = obj-id [SP [obj-val *(SP obj-val)]] + +The values in `obj-info` appear in the same order as the corresponding `attr` +lines, with exactly one value per requested attribute. If the server does not recognize the OID, the response will be ` SP` regardless of the number of attributes requested. diff --git a/Documentation/technical/api-parse-options.adoc b/Documentation/technical/api-parse-options.adoc index 880eb94642587a..95b7924e84e2c8 100644 --- a/Documentation/technical/api-parse-options.adoc +++ b/Documentation/technical/api-parse-options.adoc @@ -150,8 +150,77 @@ Data Structure The main data structure is an array of the `option` struct, say `static struct option builtin_add_options[]`. + +Option flags +~~~~~~~~~~~~ + +Each option can carry flags in the `flags` field of its `option` +struct. These are per-option flags and are distinct from the +`parse_options()` flags described above; they are usually set through +the `OPT_*_F()` macro variants (see below) rather than by hand. They +are the bitwise-or of: + +`PARSE_OPT_OPTARG`:: + The option's argument is optional, i.e. both `--option` and + `--option=` are accepted. + +`PARSE_OPT_NOARG`:: + The option takes no argument at all. Using `--option=` + is rejected. + +`PARSE_OPT_NONEG`:: + Disable the automatically generated negated `--no-option` + form. + +`PARSE_OPT_HIDDEN`:: + Hide the option: it is omitted from the usage shown by + `git -h`, but is still shown by `git --help-all`. + The option is parsed as usual either way. This is meant for + deprecated, advanced or otherwise uncommon options. + +`PARSE_OPT_LASTARG_DEFAULT`:: + The no-argument form is only accepted when the option is the + last token on the command line; used earlier, it still + requires an argument. Should not be combined with + `PARSE_OPT_OPTARG`. + +`PARSE_OPT_NODASH`:: + The option is a single character without a leading dash, such + as the `+` used by some commands. + +`PARSE_OPT_LITERAL_ARGHELP`:: + Use the argument help string (`argh`) verbatim in the usage + output instead of surrounding it with `<>` or `[]`. Useful when + `argh` already contains a hand-formatted description. + +`PARSE_OPT_FROM_ALIAS`:: + Internal flag, set on options that were expanded from a + configured alias. It should not be set by callers. + +`PARSE_OPT_NOCOMPLETE`:: + Do not offer this option for completion. + +`PARSE_OPT_COMP_ARG`:: + The option's argument, rather than the option itself, is what + should be completed. + +`PARSE_OPT_CMDMODE`:: + The option is one of several mutually exclusive "command mode" + options that share the same variable. Using more than one of + them at once is rejected. + +Macros +~~~~~~ + There are some macros to easily define options: +Many of the macros below have an `_F` variant (for example `OPT_BOOL_F`, +`OPT_STRING_F`, `OPT_INTEGER_F`, `OPT_SET_INT_F`, `OPT_BIT_F` and +`OPT_CALLBACK_F`) that takes an additional trailing `flags` argument. +That argument is the bitwise-or of the per-option flags described in the +"Option flags" section above; the non-`_F` macros are simply defined +with `flags` set to `0`. + `OPT__ABBREV(&int_var)`:: Add `--abbrev[=]`. @@ -175,10 +244,21 @@ There are some macros to easily define options: describes the group or an empty string. Start the description with an upper-case letter. +`OPT_HIDDEN_GROUP(description)`:: + Like `OPT_GROUP()`, but the group header carries + `PARSE_OPT_HIDDEN`, so it is only shown by `--help-all` and not + by `-h`. Use it to label a group that contains only hidden + options, which would otherwise show an empty header under `-h`. + `OPT_BOOL(short, long, &int_var, description)`:: Introduce a boolean option. `int_var` is set to one with `--option` and set to zero with `--no-option`. +`OPT_HIDDEN_BOOL(short, long, &int_var, description)`:: + Like `OPT_BOOL()`, but the option carries `PARSE_OPT_HIDDEN`, + so it is hidden from `-h` while still being shown by + `--help-all`. + `OPT_COUNTUP(short, long, &int_var, description)`:: Introduce a count-up option. Each use of `--option` increments `int_var`, starting from zero diff --git a/Documentation/technical/meson.build b/Documentation/technical/meson.build index ec07088c57617f..9ce11d5e484d9c 100644 --- a/Documentation/technical/meson.build +++ b/Documentation/technical/meson.build @@ -18,6 +18,7 @@ articles = [ 'multi-pack-index.adoc', 'packfile-uri.adoc', 'pack-heuristics.adoc', + 'paint-down-to-common.adoc', 'parallel-checkout.adoc', 'partial-clone.adoc', 'platform-support.adoc', diff --git a/Documentation/technical/paint-down-to-common.adoc b/Documentation/technical/paint-down-to-common.adoc new file mode 100644 index 00000000000000..c33c7ea7c1d38e --- /dev/null +++ b/Documentation/technical/paint-down-to-common.adoc @@ -0,0 +1,155 @@ +Merge-Base Computation and paint_down_to_common() +================================================== + +The function `paint_down_to_common()` in `commit-reach.c` computes merge +bases by walking the commit graph backwards from two sets of tips and +finding where their ancestry meets. + +Use cases +--------- + +Computing merge bases is used in two different ways: + + 1. *Finding all merge bases* (`merge-base --all`, `merge-tree`, + `merge`, `rebase`). A merge base is a common ancestor that is + not itself an ancestor of another common ancestor. + + 2. *Ancestry checks* (`in_merge_bases`, used by `merge-base + --is-ancestor`, `branch -d`, `fetch`). These ask: "is commit A + an ancestor of commit B?" If a common ancestor equals one of the + inputs, that input is necessarily the only merge base -- no other + common ancestor can be both as recent and not an ancestor of it. + +Both use cases share the same algorithm and implementation. + +Algorithm +--------- + +Given a commit `one` and a set of commits `twos[]`, the walk paints +commits with two colors: + + - PARENT1: reachable from `one` + - PARENT2: reachable from any commit in `twos[]` + +The walk uses a priority queue ordered by generation number +(highest first), breaking ties by commit date. Each step dequeues +the highest-priority commit and propagates its paint flags to its +parents, enqueuing any parent that gained new flags. When a +commit receives both PARENT1 and PARENT2, it is a merge-base +candidate. A candidate gains the STALE flag so its ancestors +propagate staleness -- any deeper common ancestor is necessarily +redundant. + +[[generation-regions]] +Topologically ordered and unordered generation regions +------------------------------------------------------ + +Commits fall into two regions based on whether their generation +numbers provide a topological ordering guarantee: + +.... + +------------------------------------------+ + | Unordered region | + | generation = INFINITY or V1_MAX | + | queue order: heuristic (commit date) | + +------------------------------------------+ + | + v + +------------------------------------------+ + | Ordered region | + | generation = finite, unsaturated | + | queue order: topological | + +------------------------------------------+ +.... + +In the ordered region, a child's generation is strictly greater +than its parent's. Same-generation commits are necessarily +independent, so the queue always processes children before +their parents. + +In the unordered region, parent-child pairs can share the same +generation number, so topological order is not guaranteed. The +queue uses commit-date as a heuristic, which typically produces +a reasonable traversal order but may process a parent before +its child. + +Commits not in the commit-graph have generation INFINITY; v1 +commit-graphs saturate at V1_MAX. Both place commits in the +unordered region. Any optimization that depends on generation +ordering must account for this saturation boundary. The early +exit gates compare against a topological ceiling -- +`GENERATION_NUMBER_V1_MAX` for v1 graphs and +`GENERATION_NUMBER_INFINITY` for v2 graphs -- so that saturated +commits are treated as unordered. + +With generation ordering, values in the unordered region exceed +those in the ordered region. The walk may therefore transition +from the unordered region into the ordered region, but never in +the reverse direction. Without a commit-graph, every commit has INFINITY +and the walk operates entirely in the unordered region. + +In the ordered region, paint on a dequeued commit is final -- +no future step can add flags to it. In the unordered region, +a dequeued commit may later gain additional paint. Paint flags +are only added, never removed, bounding the number of +re-enqueues per commit. + +Termination +----------- + +The walk tracks the number of commits of each type in the queue +(PARENT1-only, PARENT2-only, pending merge-base). The main loop +ends when one of the following conditions holds: + + 1. The queue is empty. + 2. The queue contains only stale entries. + 3. Generation cutoff: the dequeued commit's generation is below + a caller-supplied `min_generation` threshold. + 4. Single result: the caller only needs one merge base, one has + been found, and the walk has entered the ordered region. + 5. Side exhaustion: no pure PARENT1 or pure PARENT2 commits + remain in the queue, no pending merge-base candidates exist, + and the walk has entered the ordered region. + +Stale entry condition +~~~~~~~~~~~~~~~~~~~~~ +Once all queued entries are stale, no new merge-base candidates can +be discovered -- that requires at least one non-stale commit from +each side meeting. Continuing the walk could still invalidate +existing candidates by proving one is an ancestor of another, but +`remove_redundant()` handles that as a post-processing step, so it +is safe to exit early. + +Side-exhaustion condition +~~~~~~~~~~~~~~~~~~~~~~~~~ +A new merge-base requires commits from both sides to meet. When one +side's exclusive counter reaches zero and there are no pending +merge-base candidates, no future traversal step can produce a new +candidate. This optimization only activates in the ordered region, +where paint flags are final at visit time; in the unordered region, +a side that appears exhausted could reappear through late paint +propagation. + +Generation cutoff +~~~~~~~~~~~~~~~~~ +Some callers (notably `remove_redundant()`) supply a `min_generation` +threshold equal to the minimum generation of the input commits. +These callers only need to determine reachability among the inputs, +not find deep merge bases, so the walk can safely terminate when it +dequeues a commit below this threshold. + +Single result +~~~~~~~~~~~~~ +When only one merge base is needed and the walk is in the +ordered region with generation ordering, the first candidate +found is necessarily the highest-generation common ancestor. +No remaining commit in the queue can be a descendant of this +candidate (generation ordering guarantees children are visited +first), so it cannot be redundant and the walk can stop +immediately. + +Related documentation +--------------------- + + - `Documentation/technical/commit-graph.adoc` -- generation numbers + and the reachability closure property. diff --git a/Documentation/technical/partial-clone.adoc b/Documentation/technical/partial-clone.adoc index e513e391ea82ca..18718a384053a5 100644 --- a/Documentation/technical/partial-clone.adoc +++ b/Documentation/technical/partial-clone.adoc @@ -159,7 +159,7 @@ and prefetch those objects in bulk. - `repack` in GC has been updated to not touch promisor packfiles at all, and to only repack other objects. -- The global variable "fetch_if_missing" is used to control whether an +- The per-repository flag "fetch_if_missing" is used to control whether an object lookup will attempt to dynamically fetch a missing object or report an error. + diff --git a/Makefile b/Makefile index fac3e8879c8377..02327025201494 100644 --- a/Makefile +++ b/Makefile @@ -110,6 +110,9 @@ include shared.mak # Define USE_HOMEBREW_LIBICONV to link against libiconv installed by # Homebrew, if present. # +# Define DARWIN_REGEXEC if regexec() in your platform regex library +# leaks when fed an invalid UTF-8 sequence. +# # Define NO_APPLE_COMMON_CRYPTO if you are building on Darwin/Mac OS X # and do not want to use Apple's CommonCrypto library. This allows you # to provide your own OpenSSL library, for example from MacPorts. @@ -2045,6 +2048,10 @@ ifdef NO_PREAD COMPAT_CFLAGS += -DNO_PREAD COMPAT_OBJS += compat/pread.o endif +ifdef NO_WRITEV + COMPAT_CFLAGS += -DNO_WRITEV + COMPAT_OBJS += compat/writev.o +endif ifdef NO_FAST_WORKING_DIRECTORY BASIC_CFLAGS += -DNO_FAST_WORKING_DIRECTORY endif @@ -2278,6 +2285,10 @@ ifdef USE_ENHANCED_BASIC_REGULAR_EXPRESSIONS COMPAT_CFLAGS += -DUSE_ENHANCED_BASIC_REGULAR_EXPRESSIONS COMPAT_OBJS += compat/regcomp_enhanced.o endif +ifdef DARWIN_REGEXEC + COMPAT_OBJS += compat/darwin/regexec.o + BASIC_CFLAGS += -DDARWIN_REGEXEC +endif endif ifdef NATIVE_CRLF BASIC_CFLAGS += -DNATIVE_CRLF diff --git a/archive-tar.c b/archive-tar.c index 0fc70d13a8807e..a1c66024d4dff4 100644 --- a/archive-tar.c +++ b/archive-tar.c @@ -129,20 +129,20 @@ static void write_trailer(void) */ static int stream_blocked(struct repository *r, const struct object_id *oid) { - struct odb_read_stream *st; + struct odb_stream *st; char buf[BLOCKSIZE]; ssize_t readlen; - st = odb_read_stream_open(r->objects, oid, NULL); + st = odb_stream_from_object(r->objects, oid, NULL); if (!st) return error(_("cannot stream blob %s"), oid_to_hex(oid)); for (;;) { - readlen = odb_read_stream_read(st, buf, sizeof(buf)); + readlen = odb_stream_read(st, buf, sizeof(buf)); if (readlen <= 0) break; do_write_blocked(buf, readlen); } - odb_read_stream_close(st); + odb_stream_close(st); if (!readlen) finish_record(); return readlen; diff --git a/archive-zip.c b/archive-zip.c index 97ea8d60d6187b..6f73ca6d588e83 100644 --- a/archive-zip.c +++ b/archive-zip.c @@ -206,7 +206,7 @@ static void *zlib_deflate_raw(void *data, unsigned long size, unsigned long *compressed_size) { git_zstream stream; - unsigned long maxsize; + size_t maxsize; void *buffer; int result; @@ -309,7 +309,7 @@ static int write_zip_entry(struct archiver_args *args, enum zip_method method; unsigned char *out; void *deflated = NULL; - struct odb_read_stream *stream = NULL; + struct odb_stream *stream = NULL; unsigned long flags = 0; int is_binary = -1; const char *path_without_prefix = path + args->baselen; @@ -347,7 +347,7 @@ static int write_zip_entry(struct archiver_args *args, method = ZIP_METHOD_DEFLATE; if (!buffer) { - stream = odb_read_stream_open(args->repo->objects, oid, NULL); + stream = odb_stream_from_object(args->repo->objects, oid, NULL); if (!stream) return error(_("cannot stream blob %s"), oid_to_hex(oid)); @@ -428,7 +428,7 @@ static int write_zip_entry(struct archiver_args *args, ssize_t readlen; for (;;) { - readlen = odb_read_stream_read(stream, buf, sizeof(buf)); + readlen = odb_stream_read(stream, buf, sizeof(buf)); if (readlen <= 0) break; crc = crc32(crc, buf, readlen); @@ -438,7 +438,7 @@ static int write_zip_entry(struct archiver_args *args, buf, readlen); write_or_die(1, buf, readlen); } - odb_read_stream_close(stream); + odb_stream_close(stream); if (readlen) return readlen; @@ -461,7 +461,7 @@ static int write_zip_entry(struct archiver_args *args, zstream.avail_out = sizeof(compressed); for (;;) { - readlen = odb_read_stream_read(stream, buf, sizeof(buf)); + readlen = odb_stream_read(stream, buf, sizeof(buf)); if (readlen <= 0) break; crc = crc32(crc, buf, readlen); @@ -485,7 +485,7 @@ static int write_zip_entry(struct archiver_args *args, } } - odb_read_stream_close(stream); + odb_stream_close(stream); if (readlen) return readlen; diff --git a/bisect.c b/bisect.c index 94c7028d2a746a..9cbb3dc677cc47 100644 --- a/bisect.c +++ b/bisect.c @@ -488,6 +488,7 @@ static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START") static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG") static GIT_PATH_FUNC(git_path_bisect_terms, "BISECT_TERMS") static GIT_PATH_FUNC(git_path_bisect_first_parent, "BISECT_FIRST_PARENT") +static GIT_PATH_FUNC(git_path_bisect_reset_when_found, "BISECT_RESET_WHEN_FOUND") static void read_bisect_paths(struct strvec *array) { @@ -1019,10 +1020,12 @@ void read_bisect_terms(char **read_bad, char **read_good) die_errno(_("could not read file '%s'"), filename); } } else { - strbuf_getline_lf(&str, fp); + if (strbuf_getline_lf(&str, fp) == EOF) + die(_("could not read bad term from file '%s'"), filename); free(*read_bad); *read_bad = strbuf_detach(&str, NULL); - strbuf_getline_lf(&str, fp); + if (strbuf_getline_lf(&str, fp) == EOF) + die(_("could not read good term from file '%s'"), filename); free(*read_good); *read_good = strbuf_detach(&str, NULL); } @@ -1211,6 +1214,7 @@ int bisect_clean_state(void) unlink_or_warn(git_path_bisect_run()); unlink_or_warn(git_path_bisect_terms()); unlink_or_warn(git_path_bisect_first_parent()); + unlink_or_warn(git_path_bisect_reset_when_found()); /* * Cleanup BISECT_START last to support the --no-checkout option * introduced in the commit 4796e823a. diff --git a/builtin/bisect.c b/builtin/bisect.c index 3264e2da542698..1cfb8a794b36ec 100644 --- a/builtin/bisect.c +++ b/builtin/bisect.c @@ -24,11 +24,12 @@ static GIT_PATH_FUNC(git_path_bisect_start, "BISECT_START") static GIT_PATH_FUNC(git_path_bisect_log, "BISECT_LOG") static GIT_PATH_FUNC(git_path_bisect_names, "BISECT_NAMES") static GIT_PATH_FUNC(git_path_bisect_first_parent, "BISECT_FIRST_PARENT") +static GIT_PATH_FUNC(git_path_bisect_reset_when_found, "BISECT_RESET_WHEN_FOUND") static GIT_PATH_FUNC(git_path_bisect_run, "BISECT_RUN") #define BUILTIN_GIT_BISECT_START_USAGE \ N_("git bisect start [--term-(bad|new)= --term-(good|old)=]\n" \ - " [--no-checkout] [--first-parent] [ [...]] [--] [...]") + " [--no-checkout] [--first-parent] [--reset-when-found[=]] [ [...]] [--] [...]") #define BUILTIN_GIT_BISECT_BAD_USAGE \ N_("git bisect (bad|new|) []") #define BUILTIN_GIT_BISECT_GOOD_USAGE \ @@ -48,7 +49,7 @@ static GIT_PATH_FUNC(git_path_bisect_run, "BISECT_RUN") #define BUILTIN_GIT_BISECT_LOG_USAGE \ "git bisect log" #define BUILTIN_GIT_BISECT_RUN_USAGE \ - N_("git bisect run [...]") + N_("git bisect run [--reset-when-found[=]] [...]") #define BUILTIN_GIT_BISECT_HELP_USAGE \ "git bisect help" @@ -68,6 +69,12 @@ static const char * const git_bisect_usage[] = { NULL }; +enum reset_when_found_mode { + RESET_WHEN_FOUND_NONE, + RESET_WHEN_FOUND_TO_ORIGINAL, + RESET_WHEN_FOUND_TO_FOUND, +}; + struct add_bisect_ref_data { struct rev_info *revs; unsigned int object_flags; @@ -234,7 +241,7 @@ static int write_terms(const char *bad, const char *good) return res; } -static int bisect_reset(const char *commit) +static int bisect_reset(const char *commit, bool quiet) { struct strbuf branch = STRBUF_INIT; @@ -255,8 +262,10 @@ static int bisect_reset(const char *commit) struct child_process cmd = CHILD_PROCESS_INIT; cmd.git_cmd = 1; - strvec_pushl(&cmd.args, "checkout", "--ignore-other-worktrees", - branch.buf, "--", NULL); + strvec_pushl(&cmd.args, "checkout", "--ignore-other-worktrees", NULL); + if (quiet) + strvec_push(&cmd.args, "--quiet"); + strvec_pushl(&cmd.args, branch.buf, "--", NULL); if (run_command(&cmd)) { error(_("could not check out original" " HEAD '%s'. Try 'git bisect" @@ -267,7 +276,79 @@ static int bisect_reset(const char *commit) } strbuf_release(&branch); - return bisect_clean_state(); + return 0; +} + +static int parse_reset_when_found(const char *value, + enum reset_when_found_mode *mode) +{ + if (!strcmp(value, "original")) + *mode = RESET_WHEN_FOUND_TO_ORIGINAL; + else if (!strcmp(value, "found")) + *mode = RESET_WHEN_FOUND_TO_FOUND; + else + return error(_("invalid value for '--reset-when-found': '%s'"), + value); + + return 0; +} + +static const char *reset_when_found_mode_name(enum reset_when_found_mode mode) +{ + switch (mode) { + case RESET_WHEN_FOUND_TO_ORIGINAL: + return "original"; + case RESET_WHEN_FOUND_TO_FOUND: + return "found"; + case RESET_WHEN_FOUND_NONE: + BUG("no name for unset reset-when-found mode"); + } + BUG("unknown reset-when-found mode %d", mode); +} + +static int read_reset_when_found(enum reset_when_found_mode *mode) +{ + struct strbuf value = STRBUF_INIT; + int res = 0; + + *mode = RESET_WHEN_FOUND_NONE; + if (is_empty_or_missing_file(git_path_bisect_reset_when_found())) + return 0; + + if (strbuf_read_file(&value, git_path_bisect_reset_when_found(), 0) < 0) { + res = error_errno(_("could not read '%s'"), + git_path_bisect_reset_when_found()); + goto out; + } + strbuf_trim(&value); + if (parse_reset_when_found(value.buf, mode)) + res = -1; + +out: + strbuf_release(&value); + return res; +} + +static int bisect_reset_when_found(enum reset_when_found_mode mode) +{ + struct bisect_terms terms = { 0 }; + char *commit = NULL; + int res; + + if (mode == RESET_WHEN_FOUND_TO_FOUND) { + read_bisect_terms(&terms.term_bad, &terms.term_good); + commit = xstrfmt("refs/bisect/%s", terms.term_bad); + } else if (mode == RESET_WHEN_FOUND_NONE) { + BUG("automatic reset requested without a reset mode"); + } + + res = bisect_reset(commit, true); + if (!res) + res = bisect_clean_state(); + + free(commit); + free_terms(&terms); + return res; } static void log_commit(FILE *fp, @@ -485,7 +566,7 @@ static int bisect_next_check(const struct bisect_terms *terms, return decide_next(terms, current_term, !state.nr_good, !state.nr_bad); } -static int get_terms(struct bisect_terms *terms) +static int get_terms(struct bisect_terms *terms, int file_missing_is_ok) { struct strbuf str = STRBUF_INIT; FILE *fp = NULL; @@ -493,14 +574,21 @@ static int get_terms(struct bisect_terms *terms) fp = fopen(git_path_bisect_terms(), "r"); if (!fp) { - res = -1; + res = file_missing_is_ok ? 0 : -1; goto finish; } free_terms(terms); - strbuf_getline_lf(&str, fp); + if (strbuf_getline_lf(&str, fp) == EOF) { + res = -1; + goto finish; + } terms->term_bad = strbuf_detach(&str, NULL); - strbuf_getline_lf(&str, fp); + if (strbuf_getline_lf(&str, fp) == EOF) { + res = -1; + FREE_AND_NULL(terms->term_bad); + goto finish; + } terms->term_good = strbuf_detach(&str, NULL); finish: @@ -512,7 +600,7 @@ static int get_terms(struct bisect_terms *terms) static int bisect_terms(struct bisect_terms *terms, const char *option) { - if (get_terms(terms)) + if (get_terms(terms, 0)) return error(_("no terms defined")); if (!option) { @@ -680,7 +768,8 @@ static int bisect_successful(struct bisect_terms *terms) return res; } -static enum bisect_error bisect_next(struct bisect_terms *terms, const char *prefix) +static enum bisect_error bisect_next(struct bisect_terms *terms, + const char *prefix) { enum bisect_error res; @@ -703,7 +792,8 @@ static enum bisect_error bisect_next(struct bisect_terms *terms, const char *pre return res; } -static enum bisect_error bisect_auto_next(struct bisect_terms *terms, const char *prefix) +static enum bisect_error bisect_auto_next(struct bisect_terms *terms, + const char *prefix) { if (bisect_next_check(terms, NULL)) { bisect_print_status(terms); @@ -727,6 +817,7 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, struct strbuf bisect_names = STRBUF_INIT; struct object_id head_oid; struct object_id oid; + enum reset_when_found_mode reset_when_found = RESET_WHEN_FOUND_NONE; const char *head; if (is_bare_repository(the_repository)) @@ -750,6 +841,13 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, no_checkout = 1; } else if (!strcmp(arg, "--first-parent")) { first_parent_only = 1; + } else if (!strcmp(arg, "--reset-when-found")) { + reset_when_found = RESET_WHEN_FOUND_TO_ORIGINAL; + } else if (skip_prefix(arg, "--reset-when-found=", &arg)) { + if (parse_reset_when_found(arg, &reset_when_found)) { + res = BISECT_FAILED; + goto finish; + } } else if (!strcmp(arg, "--term-good") || !strcmp(arg, "--term-old")) { i++; @@ -787,6 +885,11 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, break; } } + if (reset_when_found != RESET_WHEN_FOUND_NONE && no_checkout) { + res = error(_("options '%s' and '%s' cannot be used together"), + "--reset-when-found", "--no-checkout"); + goto finish; + } pathspec_pos = i; /* @@ -866,6 +969,10 @@ static enum bisect_error bisect_start(struct bisect_terms *terms, int argc, if (first_parent_only) write_file(git_path_bisect_first_parent(), "\n"); + if (reset_when_found != RESET_WHEN_FOUND_NONE) + write_file(git_path_bisect_reset_when_found(), "%s\n", + reset_when_found_mode_name(reset_when_found)); + if (no_checkout) { if (repo_get_oid(the_repository, start_head.buf, &oid) < 0) { res = error(_("invalid ref: '%s'"), start_head.buf); @@ -1057,7 +1164,8 @@ static int process_replay_line(struct bisect_terms *terms, struct strbuf *line) rev = word_end + strspn(word_end, " \t"); *word_end = '\0'; /* NUL-terminate the word */ - get_terms(terms); + if (get_terms(terms, 1)) + return error(_("no terms defined")); if (check_and_set_terms(terms, p)) return -1; @@ -1096,7 +1204,7 @@ static enum bisect_error bisect_replay(struct bisect_terms *terms, const char *f if (is_empty_or_missing_file(filename)) return error(_("cannot read file '%s' for replaying"), filename); - if (bisect_reset(NULL)) + if (bisect_clean_state()) return BISECT_FAILED; fp = fopen(filename, "r"); @@ -1244,13 +1352,36 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) { int res = BISECT_OK; struct strbuf command = STRBUF_INIT; + const char *reset_when_found_arg; const char *new_state; int temporary_stdout_fd, saved_stdout; int is_first_run = 1; + enum reset_when_found_mode reset_when_found = RESET_WHEN_FOUND_NONE; if (bisect_next_check(terms, NULL)) return BISECT_FAILED; + if (argc && !strcmp(argv[0], "--reset-when-found")) { + reset_when_found = RESET_WHEN_FOUND_TO_ORIGINAL; + } else if (argc && skip_prefix(argv[0], "--reset-when-found=", + &reset_when_found_arg)) { + if (parse_reset_when_found(reset_when_found_arg, + &reset_when_found)) + return BISECT_FAILED; + } + + if (reset_when_found != RESET_WHEN_FOUND_NONE && + refs_ref_exists(get_main_ref_store(the_repository), "BISECT_HEAD")) + return error(_("options '%s' and '%s' cannot be used together"), + "--reset-when-found", "--no-checkout"); + + if (reset_when_found != RESET_WHEN_FOUND_NONE) { + write_file(git_path_bisect_reset_when_found(), "%s\n", + reset_when_found_mode_name(reset_when_found)); + argc--; + argv++; + } + if (!argc) { error(_("bisect run failed: no command provided.")); return BISECT_FAILED; @@ -1307,7 +1438,14 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) fflush(stdout); saved_stdout = dup(1); - dup2(temporary_stdout_fd, 1); + if (saved_stdout < 0 || + dup2(temporary_stdout_fd, 1) < 0) { + res = error_errno(_("could not duplicate stdout")); + if (saved_stdout >= 0) + close(saved_stdout); + close(temporary_stdout_fd); + break; + } res = bisect_state(terms, 1, &new_state); @@ -1325,7 +1463,6 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) res = BISECT_OK; } else if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) { printf(_("bisect found first '%s' commit\n"), terms->term_bad); - res = BISECT_OK; } else if (res) { error(_("bisect run failed: 'git bisect %s'" " exited with error code %d"), new_state, res); @@ -1342,10 +1479,15 @@ static int bisect_run(struct bisect_terms *terms, int argc, const char **argv) static int cmd_bisect__reset(int argc, const char **argv, const char *prefix UNUSED, struct repository *repo UNUSED) { + int res; + if (argc > 1) return error(_("'%s' requires either no argument or a commit"), "git bisect reset"); - return bisect_reset(argc ? argv[0] : NULL); + res = bisect_reset(argc ? argv[0] : NULL, false); + if (res) + return res; + return bisect_clean_state(); } static int cmd_bisect__terms(int argc, const char **argv, const char *prefix UNUSED, @@ -1383,7 +1525,8 @@ static int cmd_bisect__next(int argc, const char **argv UNUSED, const char *pref if (argc) return error(_("'%s' requires 0 arguments"), "git bisect next"); - get_terms(&terms); + if (get_terms(&terms, 1)) + return error(_("no terms defined")); res = bisect_next(&terms, prefix); free_terms(&terms); return res; @@ -1417,7 +1560,8 @@ static int cmd_bisect__skip(int argc, const char **argv, const char *prefix UNUS struct bisect_terms terms = { 0 }; set_terms(&terms, "bad", "good"); - get_terms(&terms); + if (get_terms(&terms, 1)) + return error(_("no terms defined")); res = bisect_skip(&terms, argc, argv); free_terms(&terms); return res; @@ -1429,7 +1573,8 @@ static int cmd_bisect__visualize(int argc, const char **argv, const char *prefix int res; struct bisect_terms terms = { 0 }; - get_terms(&terms); + if (get_terms(&terms, 1)) + return error(_("no terms defined")); res = bisect_visualize(&terms, argc, argv); free_terms(&terms); return res; @@ -1443,7 +1588,8 @@ static int cmd_bisect__run(int argc, const char **argv, const char *prefix UNUSE if (!argc) return error(_("'%s' failed: no command provided."), "git bisect run"); - get_terms(&terms); + if (get_terms(&terms, 1)) + return error(_("no terms defined")); res = bisect_run(&terms, argc, argv); free_terms(&terms); return res; @@ -1482,12 +1628,14 @@ int cmd_bisect(int argc, usage_with_options(git_bisect_usage, options); set_terms(&terms, "bad", "good"); - get_terms(&terms); + if (get_terms(&terms, 1)) + return error(_("no terms defined")); if (check_and_set_terms(&terms, argv[0]) || !one_of(argv[0], terms.term_good, terms.term_bad, NULL)) usage_msg_optf(_("unknown command: '%s'"), git_bisect_usage, options, argv[0]); - res = bisect_state(&terms, argc, argv); + else + res = bisect_state(&terms, argc, argv); free_terms(&terms); } else { argc--; @@ -1495,5 +1643,15 @@ int cmd_bisect(int argc, res = fn(argc, argv, prefix, repo); } + if (res == BISECT_INTERNAL_SUCCESS_1ST_BAD_FOUND) { + enum reset_when_found_mode mode; + + if (read_reset_when_found(&mode)) + res = BISECT_FAILED; + else if (mode != RESET_WHEN_FOUND_NONE && + bisect_reset_when_found(mode)) + res = BISECT_FAILED; + } + return is_bisect_success(res) ? 0 : -res; } diff --git a/builtin/branch.c b/builtin/branch.c index 357209748d4fe4..a613148fc72786 100644 --- a/builtin/branch.c +++ b/builtin/branch.c @@ -21,6 +21,7 @@ #include "branch.h" #include "path.h" #include "string-list.h" +#include "strmap.h" #include "column.h" #include "utf8.h" #include "ref-filter.h" @@ -30,7 +31,7 @@ #include "commit-reach.h" static const char * const builtin_branch_usage[] = { - N_("git branch [] [-r | -a] [--merged] [--no-merged]"), + N_("git branch [] [-r | -a] [--merged] [--no-merged] [(--forked )...]"), N_("git branch [] [-f] [--recurse-submodules] []"), N_("git branch [] [-l] [...]"), N_("git branch [] [-r] (-d | -D) ..."), @@ -38,6 +39,8 @@ static const char * const builtin_branch_usage[] = { N_("git branch [] (-c | -C) [] "), N_("git branch [] [-r | -a] [--points-at]"), N_("git branch [] [-r | -a] [--format]"), + N_("git branch [] (--delete-merged )... " + "[...]"), NULL }; @@ -168,10 +171,13 @@ static int branch_merged(int kind, const char *name, * upstream, if any, otherwise with HEAD", we should just * return the result of the repo_in_merge_bases() above without * any of the following code, but during the transition period, - * a gentle reminder is in order. + * a gentle reminder is in order. Callers that opt out of the + * HEAD fallback by passing head_rev=NULL are not interested in + * the reminder either: they have already established that the + * branch has an upstream, so HEAD is irrelevant to the decision. */ - if (head_rev != reference_rev) { - int expect = head_rev ? repo_in_merge_bases(the_repository, rev, head_rev) : 0; + if (head_rev && head_rev != reference_rev) { + int expect = repo_in_merge_bases(the_repository, rev, head_rev); if (expect < 0) exit(128); if (expect == merged) @@ -189,20 +195,32 @@ static int branch_merged(int kind, const char *name, return merged; } +enum delete_branch_flags { + DELETE_BRANCH_FORCE = (1 << 0), + DELETE_BRANCH_QUIET = (1 << 1), + DELETE_BRANCH_SKIP_UNMERGED = (1 << 2), + DELETE_BRANCH_NO_HEAD_FALLBACK = (1 << 3), + DELETE_BRANCH_DRY_RUN = (1 << 4), +}; + static int check_branch_commit(const char *branchname, const char *refname, const struct object_id *oid, struct commit *head_rev, - int kinds, int force) + int kinds, unsigned int flags) { struct commit *rev = lookup_commit_reference(the_repository, oid); - if (!force && !rev) { + if (!(flags & DELETE_BRANCH_FORCE) && !rev) { error(_("couldn't look up commit object for '%s'"), refname); return -1; } - if (!force && !branch_merged(kinds, branchname, rev, head_rev)) { - error(_("the branch '%s' is not fully merged"), branchname); - advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH, - _("If you are sure you want to delete it, " - "run 'git branch -D %s'"), branchname); + if (!(flags & DELETE_BRANCH_FORCE) && + !branch_merged(kinds, branchname, rev, head_rev)) { + if (!(flags & DELETE_BRANCH_SKIP_UNMERGED)) { + error(_("the branch '%s' is not fully merged"), + branchname); + advise_if_enabled(ADVICE_FORCE_DELETE_BRANCH, + _("If you are sure you want to delete it, " + "run 'git branch -D %s'"), branchname); + } return -1; } return 0; @@ -217,8 +235,8 @@ static void delete_branch_config(const char *branchname) strbuf_release(&buf); } -static int delete_branches(int argc, const char **argv, int force, int kinds, - int quiet) +static int delete_branches(int argc, const char **argv, int kinds, + unsigned int flags) { struct commit *head_rev = NULL; struct object_id oid; @@ -241,7 +259,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, remote_branch = 1; allowed_interpret = INTERPRET_BRANCH_REMOTE; - force = 1; + flags |= DELETE_BRANCH_FORCE; break; case FILTER_REFS_BRANCHES: fmt = "refs/heads/%s"; @@ -252,12 +270,13 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, } branch_name_pos = strcspn(fmt, "%"); - if (!force) + if (!(flags & DELETE_BRANCH_FORCE) && + !(flags & DELETE_BRANCH_NO_HEAD_FALLBACK)) head_rev = lookup_commit_reference(the_repository, &head_oid); for (i = 0; i < argc; i++, strbuf_reset(&bname)) { char *target = NULL; - int flags = 0; + int ref_flags = 0; copy_branchname(the_repository, &bname, argv[i], allowed_interpret); @@ -287,7 +306,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, RESOLVE_REF_READING | RESOLVE_REF_NO_RECURSE | RESOLVE_REF_ALLOW_BAD_NAME, - &oid, &flags); + &oid, &ref_flags); if (!target) { if (remote_branch) { error(_("remote-tracking branch '%s' not found"), bname.buf); @@ -299,7 +318,7 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, | RESOLVE_REF_NO_RECURSE | RESOLVE_REF_ALLOW_BAD_NAME, &oid, - &flags); + &ref_flags); FREE_AND_NULL(virtual_name); if (virtual_target) @@ -314,31 +333,39 @@ static int delete_branches(int argc, const char **argv, int force, int kinds, continue; } - if (!(flags & (REF_ISSYMREF|REF_ISBROKEN)) && + if (!(ref_flags & (REF_ISSYMREF|REF_ISBROKEN)) && check_branch_commit(bname.buf, name, &oid, head_rev, kinds, - force)) { - ret = 1; + flags)) { + if (!(flags & DELETE_BRANCH_SKIP_UNMERGED)) + ret = 1; goto next; } item = string_list_append(&refs_to_delete, name); - item->util = xstrdup((flags & REF_ISBROKEN) ? "broken" - : (flags & REF_ISSYMREF) ? target + item->util = xstrdup((ref_flags & REF_ISBROKEN) ? "broken" + : (ref_flags & REF_ISSYMREF) ? target : repo_find_unique_abbrev(the_repository, &oid, DEFAULT_ABBREV)); next: free(target); } - if (refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF)) + if (!(flags & DELETE_BRANCH_DRY_RUN) && + refs_delete_refs(get_main_ref_store(the_repository), NULL, &refs_to_delete, REF_NO_DEREF)) ret = 1; for_each_string_list_item(item, &refs_to_delete) { char *describe_ref = item->util; char *name = item->string; - if (!refs_ref_exists(get_main_ref_store(the_repository), name)) { + if (flags & DELETE_BRANCH_DRY_RUN) { + if (!(flags & DELETE_BRANCH_QUIET)) + printf(remote_branch + ? _("Would delete remote-tracking branch %s (was %s).\n") + : _("Would delete branch %s (was %s).\n"), + name + branch_name_pos, describe_ref); + } else if (!refs_ref_exists(get_main_ref_store(the_repository), name)) { char *refname = name + branch_name_pos; - if (!quiet) + if (!(flags & DELETE_BRANCH_QUIET)) printf(remote_branch ? _("Deleted remote-tracking branch %s (was %s).\n") : _("Deleted branch %s (was %s).\n"), @@ -681,6 +708,208 @@ static void copy_or_rename_branch(const char *oldname, const char *newname, int free_worktrees(worktrees); } +static int parse_opt_forked(const struct option *opt, const char *arg, int unset) +{ + struct ref_filter *filter = opt->value; + + BUG_ON_OPT_NEG(unset); + if (ref_filter_forked_add(filter, arg) < 0) + die(_("'%s' is not a valid branch or pattern"), arg); + return 0; +} + +struct stacked_branch_data { + struct strset *deletable_branch_names; + struct strset *protected_branch_names; +}; + +static int collect_stacked_branch_base(const struct reference *ref, + void *cb_data) +{ + struct stacked_branch_data *data = cb_data; + const char *branch_name; + struct branch *branch; + const char *upstream_refname; + const char *upstream_branch_name; + + if (!skip_prefix(ref->name, "refs/heads/", &branch_name)) + BUG("expected local branch ref, got '%s'", ref->name); + if (strset_contains(data->deletable_branch_names, branch_name)) + return 0; + + branch = branch_get(branch_name); + upstream_refname = branch_get_upstream(branch, NULL); + if (!upstream_refname || + !skip_prefix(upstream_refname, "refs/heads/", + &upstream_branch_name) || + !strset_contains(data->deletable_branch_names, + upstream_branch_name)) + return 0; + + strset_add(data->protected_branch_names, upstream_branch_name); + return 0; +} + +static void protect_stacked_branch_bases(struct ref_store *refs, + struct strset *deletable_branch_names, + struct strset *protected_branch_names) +{ + struct stacked_branch_data data = { + .deletable_branch_names = deletable_branch_names, + .protected_branch_names = protected_branch_names, + }; + struct refs_for_each_ref_options opts = { + .prefix = "refs/heads/", + }; + struct hashmap_iter iter; + struct strmap_entry *entry; + + refs_for_each_ref_ext(refs, collect_stacked_branch_base, &data, &opts); + + strset_for_each_entry(protected_branch_names, &iter, entry) + strset_remove(deletable_branch_names, entry->key); +} + +static void clear_deleted_upstreams(struct strset *protected_branch_names, + struct strset *deletable_branch_names) +{ + struct strbuf key = STRBUF_INIT; + struct hashmap_iter iter; + struct strmap_entry *entry; + + strset_for_each_entry(protected_branch_names, &iter, entry) { + struct branch *branch = branch_get(entry->key); + const char *upstream_refname = branch_get_upstream(branch, NULL); + const char *upstream_branch_name; + + if (!upstream_refname || + !skip_prefix(upstream_refname, "refs/heads/", + &upstream_branch_name) || + !strset_contains(deletable_branch_names, + upstream_branch_name)) + continue; + + strbuf_addf(&key, "branch.%s.merge", branch->name); + repo_config_set_gently(the_repository, key.buf, NULL); + strbuf_reset(&key); + strbuf_addf(&key, "branch.%s.remote", branch->name); + repo_config_set_gently(the_repository, key.buf, NULL); + strbuf_reset(&key); + } + + strbuf_release(&key); +} + +static int branch_pushes_to_upstream(struct branch *branch, + const char *upstream) +{ + struct remote *remote = remote_get(remote_for_branch(branch, NULL)); + char *push_refname = NULL; + char *tracking = NULL; + int ret = 0; + + if (!remote) + return 0; + if (remote->push.nr) + push_refname = apply_refspecs(&remote->push, branch->refname); + else + push_refname = xstrdup(branch->refname); + if (push_refname) + tracking = apply_refspecs(&remote->fetch, push_refname); + if (tracking && !strcmp(tracking, upstream)) + ret = 1; + + free(push_refname); + free(tracking); + return ret; +} + +static int delete_merged_branches(const struct strvec *upstreams, + const char **argv, unsigned int flags) +{ + struct ref_store *refs = get_main_ref_store(the_repository); + struct ref_filter filter = REF_FILTER_INIT; + struct ref_array candidates = { 0 }; + struct strset deletable_branch_names = STRSET_INIT; + struct strset protected_branch_names = STRSET_INIT; + struct strvec branches_to_delete = STRVEC_INIT; + struct strbuf key = STRBUF_INIT; + struct hashmap_iter iter; + struct strmap_entry *entry; + int ret = 0; + + for (size_t i = 0; i < upstreams->nr; i++) + if (ref_filter_forked_add(&filter, upstreams->v[i]) < 0) + die(_("'%s' is not a valid branch or pattern"), + upstreams->v[i]); + + filter.kind = FILTER_REFS_BRANCHES; + filter.name_patterns = argv; + filter_refs(&candidates, &filter, filter.kind); + + for (int i = 0; i < candidates.nr; i++) { + const char *branch_refname = candidates.items[i]->refname; + const char *branch_name; + struct branch *branch; + const char *upstream_refname; + int opt_out; + + if (!skip_prefix(branch_refname, "refs/heads/", &branch_name)) + BUG("filter returned non-branch ref '%s'", branch_refname); + if (branch_checked_out(branch_refname)) + continue; + + branch = branch_get(branch_name); + upstream_refname = branch_get_upstream(branch, NULL); + if (!upstream_refname || !refs_ref_exists(refs, upstream_refname)) + continue; + if (branch_pushes_to_upstream(branch, upstream_refname)) + continue; + if (check_branch_commit(branch_name, branch_name, + &candidates.items[i]->objectname, NULL, + FILTER_REFS_BRANCHES, DELETE_BRANCH_SKIP_UNMERGED)) + continue; + + strbuf_reset(&key); + strbuf_addf(&key, "branch.%s.deletemerged", branch_name); + if (!repo_config_get_bool(the_repository, key.buf, &opt_out) && + !opt_out) { + if (!(flags & DELETE_BRANCH_QUIET)) + fprintf(stderr, + _("Skipping '%s' (branch.%s.deleteMerged is false)\n"), + branch_name, branch_name); + continue; + } + + strset_add(&deletable_branch_names, branch_name); + } + + protect_stacked_branch_bases(refs, &deletable_branch_names, + &protected_branch_names); + + strset_for_each_entry(&deletable_branch_names, &iter, entry) + strvec_push(&branches_to_delete, entry->key); + + if (branches_to_delete.nr) + ret = delete_branches(branches_to_delete.nr, branches_to_delete.v, + FILTER_REFS_BRANCHES, + DELETE_BRANCH_SKIP_UNMERGED | + DELETE_BRANCH_NO_HEAD_FALLBACK | + flags); + + if (!ret && !(flags & DELETE_BRANCH_DRY_RUN)) + clear_deleted_upstreams(&protected_branch_names, + &deletable_branch_names); + + strbuf_release(&key); + strvec_clear(&branches_to_delete); + strset_clear(&protected_branch_names); + strset_clear(&deletable_branch_names); + ref_array_clear(&candidates); + ref_filter_clear(&filter); + return ret; +} + static GIT_PATH_FUNC(edit_description, "EDIT_DESCRIPTION") static int edit_branch_description(const char *branch_name) @@ -745,6 +974,8 @@ int cmd_branch(int argc, /* possible actions */ int delete = 0, rename = 0, copy = 0, list = 0, unset_upstream = 0, show_current = 0, edit_description = 0; + struct strvec delete_merged = STRVEC_INIT; + int dry_run = 0; const char *new_upstream = NULL; int noncreate_actions = 0; /* possible options */ @@ -798,9 +1029,17 @@ int cmd_branch(int argc, OPT_BOOL(0, "create-reflog", &reflog, N_("create the branch's reflog")), OPT_BOOL(0, "edit-description", &edit_description, N_("edit the description for the branch")), + OPT_CALLBACK_F(0, "delete-merged", &delete_merged, N_("pattern"), + N_("delete merged branches whose upstream matches (repeatable)"), + PARSE_OPT_NONEG, parse_opt_strvec), + OPT_BOOL(0, "dry-run", &dry_run, + N_("with --delete-merged, only print which branches would be deleted")), OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE), OPT_MERGED(&filter, N_("print only branches that are merged")), OPT_NO_MERGED(&filter, N_("print only branches that are not merged")), + OPT_CALLBACK_F(0, "forked", &filter, N_("branch"), + N_("print only branches whose upstream matches (repeatable)"), + PARSE_OPT_NONEG, parse_opt_forked), OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")), OPT_REF_SORT(&sorting_options), OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"), @@ -842,19 +1081,24 @@ int cmd_branch(int argc, 0); if (!delete && !rename && !copy && !edit_description && !new_upstream && - !show_current && !unset_upstream && argc == 0) + !show_current && !unset_upstream && !delete_merged.nr && + argc == 0) list = 1; if (filter.with_commit || filter.no_commit || - filter.reachable_from || filter.unreachable_from || filter.points_at.nr) + filter.reachable_from || filter.unreachable_from || + filter.points_at.nr || filter.forked.nr) list = 1; noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream + !!show_current + !!list + !!edit_description + - !!unset_upstream; + !!unset_upstream + !!delete_merged.nr; if (noncreate_actions > 1) usage_with_options(builtin_branch_usage, options); + if (dry_run && !delete_merged.nr) + die(_("--dry-run requires --delete-merged")); + if (recurse_submodules_explicit) { if (!submodule_propagate_branches) die(_("branch with --recurse-submodules can only be used if submodule.propagateBranches is enabled")); @@ -889,7 +1133,14 @@ int cmd_branch(int argc, if (delete) { if (!argc) die(_("branch name required")); - ret = delete_branches(argc, argv, delete > 1, filter.kind, quiet); + ret = delete_branches(argc, argv, filter.kind, + (delete > 1 ? DELETE_BRANCH_FORCE : 0) | + (quiet ? DELETE_BRANCH_QUIET : 0)); + goto out; + } else if (delete_merged.nr) { + ret = delete_merged_branches(&delete_merged, argv, + (quiet ? DELETE_BRANCH_QUIET : 0) | + (dry_run ? DELETE_BRANCH_DRY_RUN : 0)); goto out; } else if (show_current) { print_current_branch_name(); @@ -1062,6 +1313,7 @@ int cmd_branch(int argc, ret = 0; out: + strvec_clear(&delete_merged); string_list_clear(&sorting_options, 0); return ret; } diff --git a/builtin/cat-file.c b/builtin/cat-file.c index e85e2cfc1b0484..8870a210ec2e94 100644 --- a/builtin/cat-file.c +++ b/builtin/cat-file.c @@ -31,6 +31,7 @@ #include "alias.h" #include "remote.h" #include "transport.h" +#include "fetch-object-info.h" /* * Maximum length for a remote URL. While no universal standard exists, @@ -681,9 +682,8 @@ static void batch_one_object(const char *obj_name, static int get_remote_info(int argc, const char **argv, - struct object_info **remote_object_info, - struct oid_array *object_info_oids, - struct string_list *object_info_options) + struct fetch_object_info_results *results, + struct oid_array *object_info_oids) { int retval = 0; struct remote *remote = NULL; @@ -724,12 +724,8 @@ static int get_remote_info(int argc, goto cleanup; } - CALLOC_ARRAY(*remote_object_info, object_info_oids->nr); - gtransport->smart_options->object_info_oids = object_info_oids; - - gtransport->smart_options->object_info_options = object_info_options; - gtransport->smart_options->object_info_data = *remote_object_info; - retval = transport_fetch_object_info(gtransport); + retval = transport_fetch_object_info(gtransport, object_info_oids, + results); cleanup: transport_disconnect(gtransport); return retval; @@ -819,21 +815,6 @@ static void parse_cmd_mailmap(struct batch_options *opt UNUSED, load_mailmap(); } -struct protocol_placeholder_entry { - const char *option; - const char *atom; -}; - -static const struct protocol_placeholder_entry remote_atom_map[] = { - {"size", "objectsize"}, - {"type", "objecttype"}, - /* - * Add new protocol options here. Even if the server doesn't support - * them the allow_list will drop them if the server doesn't advertise - * them. - */ -}; - static void parse_cmd_remote_object_info(struct batch_options *opt, const char *line, struct strbuf *output, struct expand_data *data) @@ -841,18 +822,11 @@ static void parse_cmd_remote_object_info(struct batch_options *opt, int count; const char **argv; char *line_to_split; - struct object_info *remote_object_info = NULL; + struct fetch_object_info_results results = FETCH_OBJECT_INFO_RESULTS_INIT; struct oid_array object_info_oids = OID_ARRAY_INIT; - struct string_list object_info_options = STRING_LIST_INIT_NODUP; - const char *saved_format = opt->format; if (strlen(line) >= MAX_REMOTE_OBJ_INFO_LINE) die(_("remote-object-info command too long")); - /* - * TODO: Use the default format once %(objecttype) is supported. - */ - if (!opt->format) - opt->format = "%(objectname) %(objectsize)"; line_to_split = xstrdup(line); count = split_cmdline(line_to_split, &argv); @@ -864,26 +838,25 @@ static void parse_cmd_remote_object_info(struct batch_options *opt, MAX_ALLOWED_OBJ_LIMIT); if (data->info.sizep) - string_list_append(&object_info_options, "size"); + results.wants_size = 1; if (data->info.typep) - string_list_append(&object_info_options, "type"); + results.wants_type = 1; - if (get_remote_info(count, argv, &remote_object_info, - &object_info_oids, &object_info_options)) + if (get_remote_info(count, argv, &results, &object_info_oids)) die(_("failed to get object info from the remote: %s"), argv[0]); string_list_clear(&data->remote_allowed_atoms, 0); string_list_append(&data->remote_allowed_atoms, "objectname"); - for (size_t i = 0; i < ARRAY_SIZE(remote_atom_map); i++) - if (unsorted_string_list_has_string(&object_info_options, remote_atom_map[i].option)) - string_list_append(&data->remote_allowed_atoms, - remote_atom_map[i].atom); + if (results.sizes) + string_list_append(&data->remote_allowed_atoms, "objectsize"); + if (results.types) + string_list_append(&data->remote_allowed_atoms, "objecttype"); data->skip_object_info = 1; - for (size_t i = 0; i < object_info_oids.nr; i++) { + for (size_t i = 0; i < results.nr; i++) { data->oid = object_info_oids.oid[i]; - if (remote_object_info[i].unrecognized) { + if (results.unrecognized[i]) { report_object_status(opt, oid_to_hex(&data->oid), &data->oid, "missing"); continue; @@ -893,13 +866,11 @@ static void parse_cmd_remote_object_info(struct batch_options *opt, * When reaching here, it means remote-object-info can retrieve * information from server without downloading them. */ - if (remote_object_info[i].sizep) { - data->size = *remote_object_info[i].sizep; - } + if (results.sizes) + data->size = results.sizes[i]; - if (remote_object_info[i].typep) { - data->type = *remote_object_info[i].typep; - } + if (results.types) + data->type = results.types[i]; opt->batch_mode = BATCH_MODE_INFO; data->is_remote = 1; @@ -907,14 +878,10 @@ static void parse_cmd_remote_object_info(struct batch_options *opt, data->is_remote = 0; } data->skip_object_info = 0; - opt->format = saved_format; - for (size_t i = 0; i < object_info_oids.nr; i++) - free_object_info_contents(&remote_object_info[i]); - string_list_clear(&object_info_options, 0); + free_fetch_object_info_results(&results); free(line_to_split); free(argv); - free(remote_object_info); oid_array_clear(&object_info_oids); } diff --git a/builtin/checkout.c b/builtin/checkout.c index 55e3a89a852712..09b3c87e2a1280 100644 --- a/builtin/checkout.c +++ b/builtin/checkout.c @@ -838,10 +838,11 @@ static void init_topts(struct unpack_trees_options *topts, static int merge_working_tree(const struct checkout_opts *opts, struct branch_info *old_branch_info, struct branch_info *new_branch_info, - bool quiet, + bool allow_autostash, int *writeout_error) { int ret; + bool can_autostash = false; struct lock_file lock_file = LOCK_INIT; struct tree *new_tree; @@ -888,9 +889,13 @@ static int merge_working_tree(const struct checkout_opts *opts, return 1; } + if (allow_autostash) + can_autostash = has_unstaged_changes(the_repository, 1) || + has_uncommitted_changes(the_repository, 1); + /* 2-way merge to the new branch */ init_topts(&topts, opts->show_progress, - opts->overwrite_ignore, quiet); + opts->overwrite_ignore, can_autostash); init_checkout_metadata(&topts.meta, new_branch_info->refname, new_branch_info->commit ? &new_branch_info->commit->object.oid : @@ -917,7 +922,8 @@ static int merge_working_tree(const struct checkout_opts *opts, clear_unpack_trees_porcelain(&topts); if (ret == -1) { rollback_lock_file(&lock_file); - return MERGE_WORKING_TREE_UNPACK_FAILED; + return can_autostash ? + MERGE_WORKING_TREE_UNPACK_FAILED : 1; } } @@ -1166,6 +1172,7 @@ static int switch_branches(const struct checkout_opts *opts, int flag, writeout_error = 0; int do_merge = 1; int created_autostash = 0; + bool autostash_conflicted = false; struct strbuf old_commit_shortname = STRBUF_INIT; struct strbuf autostash_msg = STRBUF_INIT; const char *stash_label_base = NULL; @@ -1242,7 +1249,8 @@ static int switch_branches(const struct checkout_opts *opts, new_branch_info->name, "local", stash_label_base, - autostash_msg.buf); + autostash_msg.buf, + &autostash_conflicted); } if (ret) { branch_info_release(&old_branch_info); @@ -1255,6 +1263,8 @@ static int switch_branches(const struct checkout_opts *opts, if (!opts->quiet && !old_branch_info.path && old_branch_info.commit && new_branch_info->commit != old_branch_info.commit) orphaned_commit_warning(old_branch_info.commit, new_branch_info->commit); + if (autostash_conflicted && !opts->quiet) + fputc('\n', stderr); update_refs_for_switch(opts, &old_branch_info, new_branch_info); if (created_autostash) { diff --git a/builtin/commit.c b/builtin/commit.c index 28f61745034506..d678a81865a6e0 100644 --- a/builtin/commit.c +++ b/builtin/commit.c @@ -1980,7 +1980,7 @@ int cmd_commit(int argc, } apply_autostash_ref(the_repository, "MERGE_AUTOSTASH", - NULL, NULL, NULL, NULL); + NULL, NULL, NULL, NULL, NULL); cleanup: free_commit_extra_headers(extra); diff --git a/builtin/config.c b/builtin/config.c index 0882899c3fbd2a..2554322317163c 100644 --- a/builtin/config.c +++ b/builtin/config.c @@ -1313,7 +1313,10 @@ static int show_editor(struct config_location_options *opts) else if (errno != EEXIST) die_errno(_("cannot create configuration file %s"), config_file); } - launch_editor(config_file, NULL, NULL); + if (launch_editor(config_file, NULL, NULL)) { + free(config_file); + return -1; + } free(config_file); return 0; diff --git a/builtin/fast-import.c b/builtin/fast-import.c index 6692f7cd812d0e..fbd919982c956a 100644 --- a/builtin/fast-import.c +++ b/builtin/fast-import.c @@ -30,6 +30,7 @@ #include "khash.h" #include "date.h" #include "gpg-interface.h" +#include "parse-options.h" #define PACK_ID_BITS 16 #define MAX_PACK_ID ((1<argc = argc; + state->argv = argv; + state->prefix = prefix; + state->option = option; +} + +static void parse_argv(struct fast_import_state *state); +static void parse_get_mark(struct fast_import_state *state, const char *p); +static void parse_cat_blob(struct fast_import_state *state, const char *p); +static void parse_ls(struct fast_import_state *state, + const char *p, + struct branch *b); static void for_each_mark(struct mark_set *m, uintmax_t base, each_mark_fn_t callback, void *p) { @@ -962,7 +980,7 @@ static int store_object( struct object_entry *e; unsigned char hdr[96]; struct object_id oid; - unsigned long hdrlen, deltalen; + unsigned long hdrlen, deltalen = 0; struct git_hash_ctx c; git_zstream s; struct repo_config_values *cfg = repo_config_values(the_repository); @@ -998,11 +1016,13 @@ static int store_object( if (last && last->data.len && last->data.buf && last->depth < max_depth && dat->len > the_hash_algo->rawsz) { + size_t deltalen_st; delta_count_attempts_by_type[type]++; delta = diff_delta(last->data.buf, last->data.len, dat->buf, dat->len, - &deltalen, dat->len - the_hash_algo->rawsz); + &deltalen_st, dat->len - the_hash_algo->rawsz); + deltalen = cast_size_t_to_ulong(deltalen_st); } else delta = NULL; @@ -1845,7 +1865,7 @@ static void read_marks(void) } -static int read_next_command(void) +static int read_next_command(struct fast_import_state *state) { static int stdin_eof = 0; @@ -1864,10 +1884,10 @@ static int read_next_command(void) if (stdin_eof) return EOF; - if (!seen_data_command + if (!state->seen_data_command && !starts_with(command_buf.buf, "feature ") && !starts_with(command_buf.buf, "option ")) { - parse_argv(); + parse_argv(state); } rc = rc_free; @@ -1899,22 +1919,22 @@ static void skip_optional_lf(void) ungetc(term_char, stdin); } -static void parse_mark(void) +static void parse_mark(struct fast_import_state *state) { const char *v; if (skip_prefix(command_buf.buf, "mark :", &v)) { next_mark = strtoumax(v, NULL, 10); - read_next_command(); + read_next_command(state); } else next_mark = 0; } -static void parse_original_identifier(void) +static void parse_original_identifier(struct fast_import_state *state) { const char *v; if (skip_prefix(command_buf.buf, "original-oid ", &v)) - read_next_command(); + read_next_command(state); } static int parse_data(struct strbuf *sb, uintmax_t limit, uintmax_t *len_res) @@ -2068,11 +2088,11 @@ static void parse_and_store_blob( } } -static void parse_new_blob(void) +static void parse_new_blob(struct fast_import_state *state) { - read_next_command(); - parse_mark(); - parse_original_identifier(); + read_next_command(state); + parse_mark(state); + parse_original_identifier(state); parse_and_store_blob(&last_blob, NULL, next_mark); } @@ -2368,7 +2388,9 @@ static void parse_path_space(struct strbuf *sb, const char *p, (*endp)++; } -static void file_change_m(const char *p, struct branch *b) +static void file_change_m(struct fast_import_state *state, + const char *p, + struct branch *b) { static struct strbuf path = STRBUF_INIT; struct object_entry *oe; @@ -2435,10 +2457,10 @@ static void file_change_m(const char *p, struct branch *b) if (S_ISDIR(mode)) die(_("directories cannot be specified 'inline': %s"), command_buf.buf); - while (read_next_command() != EOF) { + while (read_next_command(state) != EOF) { const char *v; if (skip_prefix(command_buf.buf, "cat-blob ", &v)) - parse_cat_blob(v); + parse_cat_blob(state, v); else { parse_and_store_blob(&last_blob, &oid, 0); break; @@ -2512,7 +2534,10 @@ static void file_change_cr(const char *p, struct branch *b, int rename) leaf.tree); } -static void note_change_n(const char *p, struct branch *b, unsigned char *old_fanout) +static void note_change_n(struct fast_import_state *state, + const char *p, + struct branch *b, + unsigned char *old_fanout) { struct object_entry *oe; struct branch *s; @@ -2577,7 +2602,7 @@ static void note_change_n(const char *p, struct branch *b, unsigned char *old_fa die(_("invalid ref name or SHA1 expression: %s"), p); if (inline_data) { - read_next_command(); + read_next_command(state); parse_and_store_blob(&last_blob, &oid, 0); } else if (oe) { if (oe->type != OBJ_BLOB) @@ -2644,7 +2669,9 @@ static void parse_from_existing(struct branch *b) } } -static int parse_objectish(struct branch *b, const char *objectish) +static int parse_objectish(struct fast_import_state *state, + struct branch *b, + const char *objectish) { struct branch *s; struct object_id oid; @@ -2687,31 +2714,34 @@ static int parse_objectish(struct branch *b, const char *objectish) b->branch_tree.tree = NULL; } - read_next_command(); + read_next_command(state); return 1; } -static int parse_from(struct branch *b) +static int parse_from(struct fast_import_state *state, struct branch *b) { const char *from; if (!skip_prefix(command_buf.buf, "from ", &from)) return 0; - return parse_objectish(b, from); + return parse_objectish(state, b, from); } -static int parse_objectish_with_prefix(struct branch *b, const char *prefix) +static int parse_objectish_with_prefix(struct fast_import_state *state, + struct branch *b, + const char *prefix) { const char *base; if (!skip_prefix(command_buf.buf, prefix, &base)) return 0; - return parse_objectish(b, base); + return parse_objectish(state, b, base); } -static struct hash_list *parse_merge(unsigned int *count) +static struct hash_list *parse_merge(struct fast_import_state *state, + unsigned int *count) { struct hash_list *list = NULL, **tail = &list, *n; const char *from; @@ -2745,7 +2775,7 @@ static struct hash_list *parse_merge(unsigned int *count) tail = &n->next; (*count)++; - read_next_command(); + read_next_command(state); } return list; } @@ -2756,7 +2786,9 @@ struct signature_data { struct strbuf data; /* The actual signature data */ }; -static void parse_one_signature(struct signature_data *sig, const char *v) +static void parse_one_signature(struct fast_import_state *state, + struct signature_data *sig, + const char *v) { char *args = xstrdup(v); /* Will be freed when sig->hash_algo is freed */ char *space = strchr(args, ' '); @@ -2781,15 +2813,15 @@ static void parse_one_signature(struct signature_data *sig, const char *v) warning(_("'unknown' signature format in gpgsig")); /* Read signature data */ - read_next_command(); + read_next_command(state); parse_data(&sig->data, 0, NULL); } -static void discard_one_signature(void) +static void discard_one_signature(struct fast_import_state *state) { struct strbuf data = STRBUF_INIT; - read_next_command(); + read_next_command(state); parse_data(&data, 0, NULL); strbuf_release(&data); } @@ -2827,13 +2859,14 @@ static void store_signature(struct signature_data *stored_sig, } } -static void import_one_signature(struct signature_data *sig_sha1, +static void import_one_signature(struct fast_import_state *state, + struct signature_data *sig_sha1, struct signature_data *sig_sha256, const char *v) { struct signature_data sig = { NULL, NULL, STRBUF_INIT }; - parse_one_signature(&sig, v); + parse_one_signature(state, &sig, v); if (!strcmp(sig.hash_algo, "sha1")) store_signature(sig_sha1, &sig, "SHA-1"); @@ -2947,7 +2980,7 @@ static void handle_signature_if_invalid(struct strbuf *new_data, strbuf_release(&tmp_buf); } -static void parse_new_commit(const char *arg) +static void parse_new_commit(struct fast_import_state *state, const char *arg) { static struct strbuf msg = STRBUF_INIT; struct signature_data sig_sha1 = { NULL, NULL, STRBUF_INIT }; @@ -2965,16 +2998,16 @@ static void parse_new_commit(const char *arg) if (!b) b = new_branch(arg); - read_next_command(); - parse_mark(); - parse_original_identifier(); + read_next_command(state); + parse_mark(state); + parse_original_identifier(state); if (skip_prefix(command_buf.buf, "author ", &v)) { author = parse_ident(v); - read_next_command(); + read_next_command(state); } if (skip_prefix(command_buf.buf, "committer ", &v)) { committer = parse_ident(v); - read_next_command(); + read_next_command(state); } if (!committer) die(_("expected committer but didn't get one")); @@ -2990,7 +3023,7 @@ static void parse_new_commit(const char *arg) warning(_("stripping a commit signature")); /* fallthru */ case SIGN_STRIP: - discard_one_signature(); + discard_one_signature(state); break; /* Second, modes that parse the signature */ @@ -3001,24 +3034,24 @@ static void parse_new_commit(const char *arg) case SIGN_STRIP_IF_INVALID: case SIGN_SIGN_IF_INVALID: case SIGN_ABORT_IF_INVALID: - import_one_signature(&sig_sha1, &sig_sha256, v); + import_one_signature(state, &sig_sha1, &sig_sha256, v); break; /* Third, BUG */ default: BUG("invalid signed_commit_mode value %d", signed_commit_mode); } - read_next_command(); + read_next_command(state); } if (skip_prefix(command_buf.buf, "encoding ", &v)) { encoding = xstrdup(v); - read_next_command(); + read_next_command(state); } parse_data(&msg, 0, NULL); - read_next_command(); - parse_from(b); - merge_list = parse_merge(&merge_count); + read_next_command(state); + parse_from(state, b); + merge_list = parse_merge(state, &merge_count); /* ensure the branch is active/loaded */ if (!b->branch_tree.tree || !max_active_branches) { @@ -3031,7 +3064,7 @@ static void parse_new_commit(const char *arg) /* file_change* */ while (command_buf.len > 0) { if (skip_prefix(command_buf.buf, "M ", &v)) - file_change_m(v, b); + file_change_m(state, v, b); else if (skip_prefix(command_buf.buf, "D ", &v)) file_change_d(v, b); else if (skip_prefix(command_buf.buf, "R ", &v)) @@ -3039,18 +3072,18 @@ static void parse_new_commit(const char *arg) else if (skip_prefix(command_buf.buf, "C ", &v)) file_change_cr(v, b, 0); else if (skip_prefix(command_buf.buf, "N ", &v)) - note_change_n(v, b, &prev_fanout); + note_change_n(state, v, b, &prev_fanout); else if (!strcmp("deleteall", command_buf.buf)) file_change_deleteall(b); else if (skip_prefix(command_buf.buf, "ls ", &v)) - parse_ls(v, b); + parse_ls(state, v, b); else if (skip_prefix(command_buf.buf, "cat-blob ", &v)) - parse_cat_blob(v); + parse_cat_blob(state, v); else { unread_command_buf = 1; break; } - if (read_next_command() == EOF) + if (read_next_command(state) == EOF) break; } @@ -3188,7 +3221,7 @@ static void handle_tag_signature(struct strbuf *buf, struct strbuf *msg, const c } } -static void parse_new_tag(const char *arg) +static void parse_new_tag(struct fast_import_state *state, const char *arg) { static struct strbuf msg = STRBUF_INIT; const char *from; @@ -3207,8 +3240,8 @@ static void parse_new_tag(const char *arg) else first_tag = t; last_tag = t; - read_next_command(); - parse_mark(); + read_next_command(state); + parse_mark(state); /* from ... */ if (!skip_prefix(command_buf.buf, "from ", &from)) @@ -3236,15 +3269,15 @@ static void parse_new_tag(const char *arg) type = oe->type; } else die(_("invalid ref name or SHA1 expression: %s"), from); - read_next_command(); + read_next_command(state); /* original-oid ... */ - parse_original_identifier(); + parse_original_identifier(state); /* tagger ... */ if (skip_prefix(command_buf.buf, "tagger ", &v)) { tagger = parse_ident(v); - read_next_command(); + read_next_command(state); } else tagger = NULL; @@ -3275,7 +3308,7 @@ static void parse_new_tag(const char *arg) t->pack_id = pack_id; } -static void parse_reset_branch(const char *arg) +static void parse_reset_branch(struct fast_import_state *state, const char *arg) { struct branch *b; const char *tag_name; @@ -3292,8 +3325,8 @@ static void parse_reset_branch(const char *arg) } else b = new_branch(arg); - read_next_command(); - parse_from(b); + read_next_command(state); + parse_from(state, b); if (b->delete && skip_prefix(b->name, "refs/tags/", &tag_name)) { /* * Elsewhere, we call dump_branches() before dump_tags(), @@ -3333,6 +3366,7 @@ static void cat_blob_write(const char *buf, unsigned long size) static void cat_blob(struct object_entry *oe, struct object_id *oid) { struct strbuf line = STRBUF_INIT; + struct iovec iov[3]; unsigned long size; enum object_type type = 0; char *buf; @@ -3366,10 +3400,21 @@ static void cat_blob(struct object_entry *oe, struct object_id *oid) strbuf_reset(&line); strbuf_addf(&line, "%s %s %"PRIuMAX"\n", oid_to_hex(oid), type_name(type), (uintmax_t)size); - cat_blob_write(line.buf, line.len); + + /* + * Write the header, the payload and the trailing newline with a + * single writev(3p) call instead of three separate write(3p) calls. + */ + iov[0].iov_base = line.buf; + iov[0].iov_len = line.len; + iov[1].iov_base = buf; + iov[1].iov_len = size; + iov[2].iov_base = (void *) "\n"; + iov[2].iov_len = 1; + + if (writev_in_full(cat_blob_fd, iov, ARRAY_SIZE(iov)) < 0) + die_errno(_("write to frontend failed")); strbuf_release(&line); - cat_blob_write(buf, size); - cat_blob_write("\n", 1); if (oe && oe->pack_id == pack_id) { last_blob.offset = oe->idx.offset; strbuf_attach(&last_blob.data, buf, size, size + 1); @@ -3378,7 +3423,8 @@ static void cat_blob(struct object_entry *oe, struct object_id *oid) free(buf); } -static void parse_get_mark(const char *p) +static void parse_get_mark(struct fast_import_state *state UNUSED, + const char *p) { struct object_entry *oe; char output[GIT_MAX_HEXSZ + 2]; @@ -3395,7 +3441,8 @@ static void parse_get_mark(const char *p) cat_blob_write(output, the_hash_algo->hexsz + 1); } -static void parse_cat_blob(const char *p) +static void parse_cat_blob(struct fast_import_state *state UNUSED, + const char *p) { struct object_entry *oe; struct object_id oid; @@ -3560,7 +3607,9 @@ static void print_ls(int mode, const unsigned char *hash, const char *path) cat_blob_write(line.buf, line.len); } -static void parse_ls(const char *p, struct branch *b) +static void parse_ls(struct fast_import_state *state UNUSED, + const char *p, + struct branch *b) { static struct strbuf path = STRBUF_INIT; struct tree_entry *root = NULL; @@ -3607,13 +3656,13 @@ static void checkpoint(void) dump_marks(); } -static void parse_checkpoint(void) +static void parse_checkpoint(struct fast_import_state *state UNUSED) { checkpoint_requested = 1; skip_optional_lf(); } -static void parse_progress(void) +static void parse_progress(struct fast_import_state *state UNUSED) { fwrite(command_buf.buf, 1, command_buf.len, stdout); fputc('\n', stdout); @@ -3621,37 +3670,40 @@ static void parse_progress(void) skip_optional_lf(); } -static void parse_alias(void) +static void parse_alias(struct fast_import_state *state) { struct object_entry *e; struct branch b; skip_optional_lf(); - read_next_command(); + read_next_command(state); /* mark ... */ - parse_mark(); + parse_mark(state); if (!next_mark) die(_("expected 'mark' command, got %s"), command_buf.buf); /* to ... */ memset(&b, 0, sizeof(b)); - if (!parse_objectish_with_prefix(&b, "to ")) + if (!parse_objectish_with_prefix(state, &b, "to ")) die(_("expected 'to' command, got %s"), command_buf.buf); e = find_object(&b.oid); assert(e); insert_mark(&marks, next_mark, e); } -static char* make_fast_import_path(const char *path) +static char* make_fast_import_path(struct fast_import_state *state, + const char *path) { if (!relative_marks_paths || is_absolute_path(path)) - return prefix_filename(global_prefix, path); + return prefix_filename(state->prefix, path); return repo_git_path(the_repository, "info/fast-import/%s", path); } -static void option_import_marks(const char *marks, - int from_stream, int ignore_missing) +static void option_import_marks(struct fast_import_state *state, + const char *marks, + int from_stream, + int ignore_missing) { if (import_marks_file) { if (from_stream) @@ -3663,7 +3715,7 @@ static void option_import_marks(const char *marks, } free(import_marks_file); - import_marks_file = make_fast_import_path(marks); + import_marks_file = make_fast_import_path(state, marks); import_marks_file_from_stream = from_stream; import_marks_file_ignore_missing = ignore_missing; } @@ -3703,13 +3755,15 @@ static void option_active_branches(const char *branches) max_active_branches = ulong_arg("--active-branches", branches); } -static void option_export_marks(const char *marks) +static void option_export_marks(struct fast_import_state *state, + const char *marks) { free(export_marks_file); - export_marks_file = make_fast_import_path(marks); + export_marks_file = make_fast_import_path(state, marks); } -static void option_cat_blob_fd(const char *fd) +static void option_cat_blob_fd(struct fast_import_state *state UNUSED, + const char *fd) { unsigned long n = ulong_arg("--cat-blob-fd", fd); if (n > (unsigned long) INT_MAX) @@ -3717,16 +3771,19 @@ static void option_cat_blob_fd(const char *fd) cat_blob_fd = (int) n; } -static void option_export_pack_edges(const char *edges) +static void option_export_pack_edges(struct fast_import_state *state, + const char *edges) { - char *fn = prefix_filename(global_prefix, edges); + char *fn = prefix_filename(state->prefix, edges); if (pack_edges) fclose(pack_edges); pack_edges = xfopen(fn, "a"); free(fn); } -static void option_rewrite_submodules(const char *arg, struct string_list *list) +static void option_rewrite_submodules(struct fast_import_state *state, + const char *arg, + struct string_list *list) { struct mark_set *ms; FILE *fp; @@ -3738,7 +3795,7 @@ static void option_rewrite_submodules(const char *arg, struct string_list *list) f++; CALLOC_ARRAY(ms, 1); - f = prefix_filename(global_prefix, f); + f = prefix_filename(state->prefix, f); fp = fopen(f, "r"); if (!fp) die_errno(_("cannot read '%s'"), f); @@ -3751,40 +3808,67 @@ static void option_rewrite_submodules(const char *arg, struct string_list *list) free(s); } -static int parse_one_option(const char *option) +static void option_max_pack_size(const char *arg) +{ + unsigned long v; + + if (!git_parse_ulong(arg, &v)) + die(_("--max-pack-size: argument must be a non-negative integer")); + if (v < 8192) { + warning(_("max-pack-size is now in bytes, assuming --max-pack-size=%lum"), v); + v *= 1024 * 1024; + } else if (v < 1024 * 1024) { + warning(_("minimum max-pack-size is 1 MiB")); + v = 1024 * 1024; + } + max_packsize = v; +} + +static void option_big_file_threshold(const char *arg) +{ + unsigned long v; + + if (!git_parse_ulong(arg, &v)) + die(_("--big-file-threshold: argument must be a non-negative integer")); + repo_settings_set_big_file_threshold(the_repository, v); +} + +static void option_signed_commits(const char *arg) +{ + if (parse_sign_mode(arg, &signed_commit_mode, &signed_commit_keyid)) + usagef(_("unknown --signed-commits mode '%s'"), arg); +} + +static void option_signed_tags(const char *arg) +{ + if (parse_sign_mode(arg, &signed_tag_mode, &signed_tag_keyid)) + usagef(_("unknown --signed-tags mode '%s'"), arg); +} + +static void option_quiet(void) +{ + show_stats = 0; + quiet = 1; +} + +static int parse_one_option(struct fast_import_state *state, const char *option) { if (skip_prefix(option, "max-pack-size=", &option)) { - unsigned long v; - if (!git_parse_ulong(option, &v)) - return 0; - if (v < 8192) { - warning(_("max-pack-size is now in bytes, assuming --max-pack-size=%lum"), v); - v *= 1024 * 1024; - } else if (v < 1024 * 1024) { - warning(_("minimum max-pack-size is 1 MiB")); - v = 1024 * 1024; - } - max_packsize = v; + option_max_pack_size(option); } else if (skip_prefix(option, "big-file-threshold=", &option)) { - unsigned long v; - if (!git_parse_ulong(option, &v)) - return 0; - repo_settings_set_big_file_threshold(the_repository, v); + option_big_file_threshold(option); } else if (skip_prefix(option, "depth=", &option)) { option_depth(option); } else if (skip_prefix(option, "active-branches=", &option)) { option_active_branches(option); } else if (skip_prefix(option, "export-pack-edges=", &option)) { - option_export_pack_edges(option); + option_export_pack_edges(state, option); } else if (skip_prefix(option, "signed-commits=", &option)) { - if (parse_sign_mode(option, &signed_commit_mode, &signed_commit_keyid)) - usagef(_("unknown --signed-commits mode '%s'"), option); + option_signed_commits(option); } else if (skip_prefix(option, "signed-tags=", &option)) { - if (parse_sign_mode(option, &signed_tag_mode, &signed_tag_keyid)) - usagef(_("unknown --signed-tags mode '%s'"), option); + option_signed_tags(option); } else if (!strcmp(option, "quiet")) { - show_stats = 0; - quiet = 1; + option_quiet(); } else if (!strcmp(option, "stats")) { show_stats = 1; } else if (!strcmp(option, "allow-unsafe-features")) { @@ -3796,34 +3880,36 @@ static int parse_one_option(const char *option) return 1; } -static void check_unsafe_feature(const char *feature, int from_stream) +static void check_unsafe_feature(struct fast_import_state *state, + const char *feature) { - if (from_stream && !allow_unsafe_features) + if (!state->allow_unsafe_features) die(_("feature '%s' forbidden in input without --allow-unsafe-features"), feature); } -static int parse_one_feature(const char *feature, int from_stream) +static int parse_one_feature(struct fast_import_state *state, + const char *feature) { const char *arg; if (skip_prefix(feature, "date-format=", &arg)) { option_date_format(arg); } else if (skip_prefix(feature, "import-marks=", &arg)) { - check_unsafe_feature("import-marks", from_stream); - option_import_marks(arg, from_stream, 0); + check_unsafe_feature(state, "import-marks"); + option_import_marks(state, arg, 1, 0); } else if (skip_prefix(feature, "import-marks-if-exists=", &arg)) { - check_unsafe_feature("import-marks-if-exists", from_stream); - option_import_marks(arg, from_stream, 1); + check_unsafe_feature(state, "import-marks-if-exists"); + option_import_marks(state, arg, 1, 1); } else if (skip_prefix(feature, "export-marks=", &arg)) { - check_unsafe_feature(feature, from_stream); - option_export_marks(arg); + check_unsafe_feature(state, feature); + option_export_marks(state, arg); } else if (!strcmp(feature, "alias")) { ; /* Don't die - this feature is supported */ } else if (skip_prefix(feature, "rewrite-submodules-to=", &arg)) { - option_rewrite_submodules(arg, &sub_marks_to); + option_rewrite_submodules(state, arg, &sub_marks_to); } else if (skip_prefix(feature, "rewrite-submodules-from=", &arg)) { - option_rewrite_submodules(arg, &sub_marks_from); + option_rewrite_submodules(state, arg, &sub_marks_from); } else if (!strcmp(feature, "get-mark")) { ; /* Don't die - this feature is supported */ } else if (!strcmp(feature, "cat-blob")) { @@ -3845,23 +3931,23 @@ static int parse_one_feature(const char *feature, int from_stream) return 1; } -static void parse_feature(const char *feature) +static void parse_feature(struct fast_import_state *state, const char *feature) { - if (seen_data_command) + if (state->seen_data_command) die(_("got feature command '%s' after data command"), feature); - if (parse_one_feature(feature, 1)) + if (parse_one_feature(state, feature)) return; die(_("this version of fast-import does not support feature %s."), feature); } -static void parse_option(const char *option) +static void parse_option(struct fast_import_state *state, const char *option) { - if (seen_data_command) + if (state->seen_data_command) die(_("got option command '%s' after data command"), option); - if (parse_one_option(option)) + if (parse_one_option(state, option)) return; die(_("this version of fast-import does not support option: %s"), option); @@ -3894,42 +3980,144 @@ static void git_pack_config(void) repo_config(the_repository, git_default_config, NULL); } -static const char fast_import_usage[] = -"git fast-import [--date-format=] [--max-pack-size=] [--big-file-threshold=] [--depth=] [--active-branches=] [--export-marks=]"; +static const char *const fast_import_usage[] = { + N_("git fast-import []"), + NULL +}; -static void parse_argv(void) +static void parse_argv(struct fast_import_state *state) { - unsigned int i; + int argc = parse_options(state->argc, state->argv, state->prefix, + state->option, fast_import_usage, + PARSE_OPT_KEEP_ARGV0); - for (i = 1; i < global_argc; i++) { - const char *a = global_argv[i]; + if (argc > 1) + usage_with_options(fast_import_usage, state->option); - if (*a != '-' || !strcmp(a, "--")) - break; + state->seen_data_command = 1; + if (import_marks_file) + read_marks(); + build_mark_map(&sub_marks_from, &sub_marks_to); +} - if (!skip_prefix(a, "--", &a)) - die(_("unknown option %s"), a); +static int option_parse_date_format(const struct option *opt UNUSED, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_date_format(arg); + return 0; +} - if (parse_one_option(a)) - continue; +static int option_parse_export_pack_edges(const struct option *opt, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_export_pack_edges(opt->value, arg); + return 0; +} - if (parse_one_feature(a, 0)) - continue; +static int option_parse_max_pack_size(const struct option *opt UNUSED, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_max_pack_size(arg); + return 0; +} - if (skip_prefix(a, "cat-blob-fd=", &a)) { - option_cat_blob_fd(a); - continue; - } +static int option_parse_big_file_threshold(const struct option *opt UNUSED, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_big_file_threshold(arg); + return 0; +} - die(_("unknown option --%s"), a); - } - if (i != global_argc) - usage(fast_import_usage); +static int option_parse_signed_commits(const struct option *opt UNUSED, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_signed_commits(arg); + return 0; +} - seen_data_command = 1; - if (import_marks_file) - read_marks(); - build_mark_map(&sub_marks_from, &sub_marks_to); +static int option_parse_signed_tags(const struct option *opt UNUSED, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_signed_tags(arg); + return 0; +} + +static int option_parse_rewrite_submodules_from(const struct option *opt, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_rewrite_submodules(opt->value, arg, &sub_marks_from); + return 0; +} + +static int option_parse_rewrite_submodules_to(const struct option *opt, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_rewrite_submodules(opt->value, arg, &sub_marks_to); + return 0; +} + +static int option_parse_cat_blob_fd(const struct option *opt, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_cat_blob_fd(opt->value, arg); + return 0; +} + +static int option_parse_import_marks(const struct option *opt, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_import_marks(opt->value, arg, 0, 0); + return 0; +} + +static int option_parse_import_marks_if_exists(const struct option *opt, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_import_marks(opt->value, arg, 0, 1); + return 0; +} + +static int option_parse_export_marks(const struct option *opt, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_export_marks(opt->value, arg); + return 0; +} + +static int option_parse_depth(const struct option *opt UNUSED, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_depth(arg); + return 0; +} + +static int option_parse_active_branches(const struct option *opt UNUSED, + const char *arg, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_active_branches(arg); + return 0; +} + +static int option_parse_quiet(const struct option *opt UNUSED, + const char *arg UNUSED, int unset) +{ + BUG_ON_OPT_NEG(unset); + option_quiet(); + return 0; } int cmd_fast_import(int argc, @@ -3937,9 +4125,80 @@ int cmd_fast_import(int argc, const char *prefix, struct repository *repo) { - unsigned int i; - - show_usage_if_asked(argc, argv, fast_import_usage); + struct fast_import_state state; + + struct option fast_import_options[] = { + OPT_GROUP(N_("Common")), + OPT_CALLBACK_F(0, "date-format", NULL, N_("fmt"), + N_("format of the commit/tag dates"), + PARSE_OPT_NONEG, option_parse_date_format), + OPT_BOOL_F(0, "stats", &show_stats, + N_("display some basic statistics (objects, packfiles and memory)"), + PARSE_OPT_NONEG), + OPT_CALLBACK_F(0, "quiet", NULL, NULL, + N_("disable the output shown by --stats"), + PARSE_OPT_NOARG | PARSE_OPT_NONEG, + option_parse_quiet), + OPT_BOOL_F(0, "force", &force_update, + N_("force updating modified existing branches"), PARSE_OPT_NONEG), + OPT_BOOL_F(0, "done", &require_explicit_termination, + N_("require a terminating 'done' command"), PARSE_OPT_NONEG), + OPT_CALLBACK_F(0, "max-pack-size", NULL, N_("n"), + N_("maximum size of each output pack file"), + PARSE_OPT_NONEG, option_parse_max_pack_size), + OPT_CALLBACK_F(0, "big-file-threshold", NULL, N_("n"), + N_("maximum size of a blob that will be deltified"), + PARSE_OPT_NONEG, option_parse_big_file_threshold), + OPT_CALLBACK_F(0, "depth", NULL, N_("n"), + N_("maximum delta depth"), + PARSE_OPT_NONEG, option_parse_depth), + OPT_CALLBACK_F(0, "active-branches", NULL, N_("n"), + N_("maximum number of branches to maintain active"), + PARSE_OPT_NONEG, option_parse_active_branches), + OPT_GROUP(N_("Marks")), + OPT_CALLBACK_F(0, "import-marks", &state, N_("file"), + N_("import marks from "), + PARSE_OPT_NONEG, option_parse_import_marks), + OPT_CALLBACK_F(0, "import-marks-if-exists", &state, N_("file"), + N_("import marks from if it exists"), + PARSE_OPT_NONEG, option_parse_import_marks_if_exists), + OPT_CALLBACK_F(0, "export-marks", &state, N_("file"), + N_("dump marks to "), + PARSE_OPT_NONEG, option_parse_export_marks), + OPT_BOOL(0, "relative-marks", &relative_marks_paths, + N_("are --(import|export)-marks= paths relative to '.git/info/fast-import'?")), + OPT_GROUP(N_("Submodule rewrite")), + OPT_CALLBACK_F(0, "rewrite-submodules-from", &state, N_("name:filename"), + N_("rewrite object IDs for submodule from "), + PARSE_OPT_NONEG, option_parse_rewrite_submodules_from), + OPT_CALLBACK_F(0, "rewrite-submodules-to", &state, N_("name:filename"), + N_("rewrite object IDs for submodule to "), + PARSE_OPT_NONEG, option_parse_rewrite_submodules_to), + OPT_GROUP(N_("Signing")), + OPT_CALLBACK_F(0, "signed-commits", NULL, N_("mode"), + N_("how to handle signed commits"), + PARSE_OPT_NONEG, option_parse_signed_commits), + OPT_CALLBACK_F(0, "signed-tags", NULL, N_("mode"), + N_("how to handle signed tags"), + PARSE_OPT_NONEG, option_parse_signed_tags), + OPT_HIDDEN_GROUP(N_("Advanced")), + OPT_BOOL_F(0, "allow-unsafe-features", &state.allow_unsafe_features, + N_("allow unsafe mark commands from the stream"), + PARSE_OPT_HIDDEN | PARSE_OPT_NONEG), + OPT_CALLBACK_F(0, "export-pack-edges", &state, N_("file"), + N_("dump edge commits to "), + PARSE_OPT_HIDDEN | PARSE_OPT_NONEG, + option_parse_export_pack_edges), + OPT_CALLBACK_F(0, "cat-blob-fd", &state, N_("fd"), + N_("write some responses to instead of stdout"), + PARSE_OPT_HIDDEN | PARSE_OPT_NONEG, + option_parse_cat_blob_fd), + OPT_END() + }; + + show_usage_with_options_if_asked(argc, argv, fast_import_usage, fast_import_options); + + fast_import_state_init(&state, argc, argv, prefix, fast_import_options); reset_pack_idx_option(&pack_idx_opts); git_pack_config(); @@ -3958,55 +4217,61 @@ int cmd_fast_import(int argc, * "feature" lines at the start of the stream (which allows the command * line to override stream data). But we must do an early parse of any * command-line options that impact how we interpret the feature lines. + * + * NEEDSWORK: This scan only matches the exact "--allow-unsafe-features" + * spelling and stops at the first argument that doesn't start with a + * dash. As parse_options() below also accepts unambiguous abbreviations + * and values separated by a space from their option, the two disagree + * for command lines like "--allow-unsafe" or "--depth 5 + * --allow-unsafe-features": parse_options() accepts the option, but + * this scan doesn't see it, so unsafe features from the stream are + * still refused. This errs on the safe side, but should be fixed by + * teaching this scan about the options that take a value. */ - for (i = 1; i < argc; i++) { + for (int i = 1; i < argc; i++) { const char *arg = argv[i]; if (*arg != '-' || !strcmp(arg, "--")) break; if (!strcmp(arg, "--allow-unsafe-features")) - allow_unsafe_features = 1; + state.allow_unsafe_features = 1; } - global_argc = argc; - global_argv = argv; - global_prefix = prefix; - rc_free = mem_pool_alloc(&fi_mem_pool, cmd_save * sizeof(*rc_free)); - for (i = 0; i < (cmd_save - 1); i++) + for (unsigned int i = 0; i < (cmd_save - 1); i++) rc_free[i].next = &rc_free[i + 1]; rc_free[cmd_save - 1].next = NULL; start_packfile(); set_die_routine(die_nicely); set_checkpoint_signal(); - while (read_next_command() != EOF) { + while (read_next_command(&state) != EOF) { const char *v; if (!strcmp("blob", command_buf.buf)) - parse_new_blob(); + parse_new_blob(&state); else if (skip_prefix(command_buf.buf, "commit ", &v)) - parse_new_commit(v); + parse_new_commit(&state, v); else if (skip_prefix(command_buf.buf, "tag ", &v)) - parse_new_tag(v); + parse_new_tag(&state, v); else if (skip_prefix(command_buf.buf, "reset ", &v)) - parse_reset_branch(v); + parse_reset_branch(&state, v); else if (skip_prefix(command_buf.buf, "ls ", &v)) - parse_ls(v, NULL); + parse_ls(&state, v, NULL); else if (skip_prefix(command_buf.buf, "cat-blob ", &v)) - parse_cat_blob(v); + parse_cat_blob(&state, v); else if (skip_prefix(command_buf.buf, "get-mark ", &v)) - parse_get_mark(v); + parse_get_mark(&state, v); else if (!strcmp("checkpoint", command_buf.buf)) - parse_checkpoint(); + parse_checkpoint(&state); else if (!strcmp("done", command_buf.buf)) break; else if (!strcmp("alias", command_buf.buf)) - parse_alias(); + parse_alias(&state); else if (starts_with(command_buf.buf, "progress ")) - parse_progress(); + parse_progress(&state); else if (skip_prefix(command_buf.buf, "feature ", &v)) - parse_feature(v); + parse_feature(&state, v); else if (skip_prefix(command_buf.buf, "option git ", &v)) - parse_option(v); + parse_option(&state, v); else if (starts_with(command_buf.buf, "option ")) /* ignore non-git options*/; else @@ -4017,8 +4282,8 @@ int cmd_fast_import(int argc, } /* argv hasn't been parsed yet, do so */ - if (!seen_data_command) - parse_argv(); + if (!state.seen_data_command) + parse_argv(&state); if (require_explicit_termination && feof(stdin)) die(_("stream ends early")); @@ -4035,9 +4300,9 @@ int cmd_fast_import(int argc, if (show_stats) { uintmax_t total_count = 0, duplicate_count = 0; - for (i = 0; i < ARRAY_SIZE(object_count_by_type); i++) + for (size_t i = 0; i < ARRAY_SIZE(object_count_by_type); i++) total_count += object_count_by_type[i]; - for (i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++) + for (size_t i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++) duplicate_count += duplicate_count_by_type[i]; fprintf(stderr, "%s statistics:\n", argv[0]); diff --git a/builtin/fetch-pack.c b/builtin/fetch-pack.c index 316badd969613f..86754296facc70 100644 --- a/builtin/fetch-pack.c +++ b/builtin/fetch-pack.c @@ -49,7 +49,7 @@ static void add_sought_entry(struct ref ***sought, int *nr, int *alloc, int cmd_fetch_pack(int argc, const char **argv, const char *prefix UNUSED, - struct repository *repo UNUSED) + struct repository *repo) { int i, ret; struct ref *fetched_refs = NULL, *remote_refs = NULL; @@ -67,8 +67,6 @@ int cmd_fetch_pack(int argc, struct packet_reader reader; enum protocol_version version; - fetch_if_missing = 0; - packet_trace_identity("fetch-pack"); memset(&args, 0, sizeof(args)); @@ -77,6 +75,8 @@ int cmd_fetch_pack(int argc, show_usage_if_asked(argc, argv, fetch_pack_usage); + repo->fetch_if_missing = 0; + for (i = 1; i < argc && *argv[i] == '-'; i++) { const char *arg = argv[i]; diff --git a/builtin/fsck.c b/builtin/fsck.c index a6c054e45bf8c0..8cfc0e8b26b61d 100644 --- a/builtin/fsck.c +++ b/builtin/fsck.c @@ -1017,15 +1017,15 @@ int cmd_fsck(int argc, .ref = NULL }; - /* fsck knows how to handle missing promisor objects */ - fetch_if_missing = 0; - errors_found = 0; disable_replace_refs(); save_commit_buffer = 0; argc = parse_options(argc, argv, prefix, fsck_opts, fsck_usage, 0); + /* fsck knows how to handle missing promisor objects */ + repo->fetch_if_missing = 0; + fsck_options_init(&fsck_walk_options, repo, FSCK_OPTIONS_DEFAULT); fsck_walk_options.walk = mark_object; diff --git a/builtin/index-pack.c b/builtin/index-pack.c index bc86925ad04340..6b2a87e2d39355 100644 --- a/builtin/index-pack.c +++ b/builtin/index-pack.c @@ -763,7 +763,7 @@ static void find_ref_delta_children(const struct object_id *oid, struct compare_data { struct object_entry *entry; - struct odb_read_stream *st; + struct odb_stream *st; unsigned char *buf; unsigned long buf_size; }; @@ -780,7 +780,7 @@ static int compare_objects(const unsigned char *buf, unsigned long size, } while (size) { - ssize_t len = odb_read_stream_read(data->st, data->buf, size); + ssize_t len = odb_stream_read(data->st, data->buf, size); if (len == 0) die(_("SHA1 COLLISION FOUND WITH %s !"), oid_to_hex(&data->entry->idx.oid)); @@ -806,14 +806,14 @@ static int check_collison(struct object_entry *entry) memset(&data, 0, sizeof(data)); data.entry = entry; - data.st = odb_read_stream_open(the_repository->objects, &entry->idx.oid, NULL); + data.st = odb_stream_from_object(the_repository->objects, &entry->idx.oid, NULL); if (!data.st) return -1; if (data.st->size != entry->size || data.st->type != entry->type) die(_("SHA1 COLLISION FOUND WITH %s !"), oid_to_hex(&entry->idx.oid)); unpack_data(entry, compare_objects, &data); - odb_read_stream_close(data.st); + odb_stream_close(data.st); free(data.buf); return 0; } @@ -1886,7 +1886,7 @@ static void repack_local_links(void) int cmd_index_pack(int argc, const char **argv, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { int i, fix_thin_pack = 0, verify = 0, stat_only = 0, rev_index; const char *curr_index; @@ -1903,15 +1903,15 @@ int cmd_index_pack(int argc, int report_end_of_input = 0; int hash_algo = 0; + show_usage_if_asked(argc, argv, index_pack_usage); + /* * index-pack never needs to fetch missing objects except when * REF_DELTA bases are missing (which are explicitly handled). It only * accesses the repo to do hash collision checks and to check which * REF_DELTA bases need to be fetched. */ - fetch_if_missing = 0; - - show_usage_if_asked(argc, argv, index_pack_usage); + (repo ? repo : the_repository)->fetch_if_missing = 0; disable_replace_refs(); diff --git a/builtin/last-modified.c b/builtin/last-modified.c index 5478182f2e95c2..3846244dfc0cc1 100644 --- a/builtin/last-modified.c +++ b/builtin/last-modified.c @@ -290,7 +290,8 @@ static void process_parent(struct last_modified *lm, { struct bitmap *active_p; - repo_parse_commit(lm->rev.repo, parent); + if (repo_parse_commit(lm->rev.repo, parent)) + return; active_p = active_paths_for(lm, parent); /* @@ -414,12 +415,14 @@ static int last_modified_run(struct last_modified *lm) * Otherwise, make sure that 'c' isn't reachable from anything * in the '--not' queue. */ - repo_parse_commit(lm->rev.repo, c); + if (repo_parse_commit(lm->rev.repo, c)) + goto cleanup; while ((n = prio_queue_get(¬_queue))) { struct commit_list *np; - repo_parse_commit(lm->rev.repo, n); + if (repo_parse_commit(lm->rev.repo, n)) + continue; for (np = n->parents; np; np = np->next) { if (!(np->item->object.flags & PARENT2)) { diff --git a/builtin/merge.c b/builtin/merge.c index 5b4eb23a833295..e21b0d8ffbc5c7 100644 --- a/builtin/merge.c +++ b/builtin/merge.c @@ -538,7 +538,7 @@ static void finish(struct commit *head_commit, if (new_head) apply_autostash_ref(the_repository, "MERGE_AUTOSTASH", - NULL, NULL, NULL, NULL); + NULL, NULL, NULL, NULL, NULL); strbuf_release(&reflog_message); } @@ -1682,7 +1682,7 @@ int cmd_merge(int argc, &commit->object.oid, overwrite_ignore)) { apply_autostash_ref(the_repository, "MERGE_AUTOSTASH", - NULL, NULL, NULL, NULL); + NULL, NULL, NULL, NULL, NULL); ret = 1; goto done; } @@ -1846,7 +1846,7 @@ int cmd_merge(int argc, fprintf(stderr, _("Merge with strategy %s failed.\n"), use_strategies[0]->name); apply_autostash_ref(the_repository, "MERGE_AUTOSTASH", - NULL, NULL, NULL, NULL); + NULL, NULL, NULL, NULL, NULL); ret = 2; goto done; } else if (best_strategy == wt_strategy) diff --git a/builtin/pack-objects.c b/builtin/pack-objects.c index 1ec5b6f206366e..2e76a0046cafdc 100644 --- a/builtin/pack-objects.c +++ b/builtin/pack-objects.c @@ -261,8 +261,8 @@ static int exclude_promisor_objects_best_effort; static int use_delta_islands; -static unsigned long delta_cache_size = 0; -static unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE; +static size_t delta_cache_size = 0; +static size_t max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE; static unsigned long cache_max_small_delta_size = 1000; static unsigned long window_memory_limit = 0; @@ -354,7 +354,8 @@ static void index_commit_for_bitmap(struct commit *commit) static void *get_delta(struct object_entry *entry) { - unsigned long size, base_size, delta_size; + unsigned long size, base_size; + size_t delta_size; void *buf, *base_buf, *delta_buf; enum object_type type; size_t size_st = 0, base_size_st = 0; @@ -411,7 +412,7 @@ static unsigned long do_compress(void **pptr, unsigned long size) return stream.total_out; } -static unsigned long write_large_blob_data(struct odb_read_stream *st, struct hashfile *f, +static unsigned long write_large_blob_data(struct odb_stream *st, struct hashfile *f, const struct object_id *oid) { git_zstream stream; @@ -425,7 +426,7 @@ static unsigned long write_large_blob_data(struct odb_read_stream *st, struct ha for (;;) { ssize_t readlen; int zret = Z_OK; - readlen = odb_read_stream_read(st, ibuf, sizeof(ibuf)); + readlen = odb_stream_read(st, ibuf, sizeof(ibuf)); if (readlen == -1) die(_("unable to read %s"), oid_to_hex(oid)); @@ -488,7 +489,7 @@ static void copy_pack_data(struct hashfile *f, off_t len) { unsigned char *in; - unsigned long avail; + size_t avail; while (len) { in = use_pack(p, w_curs, offset, &avail); @@ -521,15 +522,15 @@ static unsigned long write_no_reuse_object(struct hashfile *f, struct object_ent unsigned hdrlen; enum object_type type; void *buf; - struct odb_read_stream *st = NULL; + struct odb_stream *st = NULL; const unsigned hashsz = the_hash_algo->rawsz; if (!usable_delta) { if (oe_type(entry) == OBJ_BLOB && oe_size_greater_than(&to_pack, entry, repo_settings_get_big_file_threshold(the_repository)) && - (st = odb_read_stream_open(the_repository->objects, &entry->idx.oid, - NULL)) != NULL) { + (st = odb_stream_from_object(the_repository->objects, &entry->idx.oid, + NULL)) != NULL) { buf = NULL; type = st->type; size = st->size; @@ -589,7 +590,7 @@ static unsigned long write_no_reuse_object(struct hashfile *f, struct object_ent dheader[--pos] = 128 | (--ofs & 127); if (limit && hdrlen + sizeof(dheader) - pos + datalen + hashsz >= limit) { if (st) - odb_read_stream_close(st); + odb_stream_close(st); free(buf); return 0; } @@ -603,7 +604,7 @@ static unsigned long write_no_reuse_object(struct hashfile *f, struct object_ent */ if (limit && hdrlen + hashsz + datalen + hashsz >= limit) { if (st) - odb_read_stream_close(st); + odb_stream_close(st); free(buf); return 0; } @@ -613,7 +614,7 @@ static unsigned long write_no_reuse_object(struct hashfile *f, struct object_ent } else { if (limit && hdrlen + datalen + hashsz >= limit) { if (st) - odb_read_stream_close(st); + odb_stream_close(st); free(buf); return 0; } @@ -621,7 +622,7 @@ static unsigned long write_no_reuse_object(struct hashfile *f, struct object_ent } if (st) { datalen = write_large_blob_data(st, f, &entry->idx.oid); - odb_read_stream_close(st); + odb_stream_close(st); } else { hashwrite(f, buf, datalen); free(buf); @@ -2259,8 +2260,7 @@ static void check_object(struct object_entry *entry, uint32_t object_index) int have_base = 0; struct object_id base_ref; struct object_entry *base_entry; - unsigned long used, used_0; - unsigned long avail; + size_t used, used_0, avail; off_t ofs; unsigned char *buf, c; enum object_type type; @@ -2688,8 +2688,8 @@ struct unpacked { unsigned depth; }; -static int delta_cacheable(unsigned long src_size, unsigned long trg_size, - unsigned long delta_size) +static int delta_cacheable(size_t src_size, size_t trg_size, + size_t delta_size) { if (max_delta_cache_size && delta_cache_size + delta_size > max_delta_cache_size) return 0; @@ -2772,8 +2772,7 @@ size_t oe_get_size_slow(struct packing_data *pack, struct pack_window *w_curs; unsigned char *buf; enum object_type type; - unsigned long used, avail; - size_t size; + size_t used, avail, size; if (e->type_ != OBJ_OFS_DELTA && e->type_ != OBJ_REF_DELTA) { size_t sz; @@ -2804,11 +2803,12 @@ size_t oe_get_size_slow(struct packing_data *pack, } static int try_delta(struct unpacked *trg, struct unpacked *src, - unsigned max_depth, unsigned long *mem_usage) + unsigned max_depth, size_t *mem_usage) { struct object_entry *trg_entry = trg->entry; struct object_entry *src_entry = src->entry; - unsigned long trg_size, src_size, delta_size, sizediff, max_size, sz; + unsigned long trg_size, src_size, sizediff, max_size, sz; + size_t delta_size; unsigned ref_depth; enum object_type type; void *delta_buf; @@ -2972,9 +2972,9 @@ static unsigned int check_delta_limit(struct object_entry *me, unsigned int n) return m; } -static unsigned long free_unpacked(struct unpacked *n) +static size_t free_unpacked(struct unpacked *n) { - unsigned long freed_mem = sizeof_delta_index(n->index); + size_t freed_mem = sizeof_delta_index(n->index); free_delta_index(n->index); n->index = NULL; if (n->data) { @@ -2991,7 +2991,7 @@ static void find_deltas(struct object_entry **list, unsigned *list_size, { uint32_t i, idx = 0, count = 0; struct unpacked *array; - unsigned long mem_usage = 0; + size_t mem_usage = 0; CALLOC_ARRAY(array, window); @@ -3701,7 +3701,7 @@ static int git_pack_config(const char *k, const char *v, return 0; } if (!strcmp(k, "pack.deltacachesize")) { - max_delta_cache_size = git_config_int(k, v, ctx->kvi); + max_delta_cache_size = git_config_size_t(k, v, ctx->kvi); return 0; } if (!strcmp(k, "pack.deltacachelimit")) { @@ -4087,9 +4087,10 @@ static void stdin_packs_read_input(struct rev_info *revs, static void add_unreachable_loose_objects(struct rev_info *revs); -static void read_stdin_packs(enum stdin_packs_mode mode, int rev_list_unpacked) +static void read_stdin_packs(struct repository *repo, + enum stdin_packs_mode mode, int rev_list_unpacked) { - int prev_fetch_if_missing = fetch_if_missing; + int prev_fetch_if_missing = repo->fetch_if_missing; struct rev_info revs; /* @@ -4097,9 +4098,9 @@ static void read_stdin_packs(enum stdin_packs_mode mode, int rev_list_unpacked) * walk is best-effort though we don't want to perform backfill fetches * for them. */ - fetch_if_missing = 0; + repo->fetch_if_missing = 0; - repo_init_revisions(the_repository, &revs, NULL); + repo_init_revisions(repo, &revs, NULL); /* * Use a revision walk to fill in the namehash of objects in the include * packs. To save time, we'll avoid traversing through objects that are @@ -4145,7 +4146,7 @@ static void read_stdin_packs(enum stdin_packs_mode mode, int rev_list_unpacked) trace2_data_intmax("pack-objects", the_repository, "stdin_packs_hints", stdin_packs_hints_nr); - fetch_if_missing = prev_fetch_if_missing; + repo->fetch_if_missing = prev_fetch_if_missing; } static void add_cruft_object_entry(const struct object_id *oid, enum object_type type, @@ -4455,9 +4456,11 @@ static void show_object__ma_allow_promisor(struct object *obj, const char *name, show_object(obj, name, data); } -static int option_parse_missing_action(const struct option *opt UNUSED, +static int option_parse_missing_action(const struct option *opt, const char *arg, int unset) { + struct repository *repo = opt->value; + assert(arg); assert(!unset); @@ -4469,14 +4472,14 @@ static int option_parse_missing_action(const struct option *opt UNUSED, if (!strcmp(arg, "allow-any")) { arg_missing_action = MA_ALLOW_ANY; - fetch_if_missing = 0; + repo->fetch_if_missing = 0; fn_show_object = show_object__ma_allow_any; return 0; } if (!strcmp(arg, "allow-promisor")) { arg_missing_action = MA_ALLOW_PROMISOR; - fetch_if_missing = 0; + repo->fetch_if_missing = 0; fn_show_object = show_object__ma_allow_promisor; return 0; } @@ -5118,7 +5121,7 @@ static int parse_stdin_packs_mode(const struct option *opt, const char *arg, int cmd_pack_objects(int argc, const char **argv, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { int use_internal_rev_list = 0; int all_progress_implied = 0; @@ -5225,7 +5228,7 @@ int cmd_pack_objects(int argc, N_("write a bitmap index if possible"), WRITE_BITMAP_QUIET, PARSE_OPT_HIDDEN), OPT_PARSE_LIST_OBJECTS_FILTER(&filter_options), - OPT_CALLBACK_F(0, "missing", NULL, N_("action"), + OPT_CALLBACK_F(0, "missing", repo, N_("action"), N_("handling for missing objects"), PARSE_OPT_NONEG, option_parse_missing_action), OPT_BOOL(0, "exclude-promisor-objects", &exclude_promisor_objects, @@ -5345,7 +5348,7 @@ int cmd_pack_objects(int argc, exclude_promisor_objects_best_effort, "--exclude-promisor-objects-best-effort"); if (exclude_promisor_objects) { - fetch_if_missing = 0; + repo->fetch_if_missing = 0; /* --stdin-packs handles promisor objects separately. */ if (!stdin_packs) { @@ -5354,8 +5357,9 @@ int cmd_pack_objects(int argc, } } else if (exclude_promisor_objects_best_effort) { use_internal_rev_list = 1; - fetch_if_missing = 0; - option_parse_missing_action(NULL, "allow-any", 0); + arg_missing_action = MA_ALLOW_ANY; + repo->fetch_if_missing = 0; + fn_show_object = show_object__ma_allow_any; /* revs configured below */ } if (unpack_unreachable || keep_unreachable || pack_loose_unreachable) @@ -5471,7 +5475,7 @@ int cmd_pack_objects(int argc, progress_state = start_progress(the_repository, _("Enumerating objects"), 0); if (stdin_packs) { - read_stdin_packs(stdin_packs, rev_list_unpacked); + read_stdin_packs(repo, stdin_packs, rev_list_unpacked); } else if (cruft) { read_cruft_objects(); } else if (!use_internal_rev_list) { diff --git a/builtin/prune.c b/builtin/prune.c index 55635a891f37d6..a7e4678d117072 100644 --- a/builtin/prune.c +++ b/builtin/prune.c @@ -194,7 +194,7 @@ int cmd_prune(int argc, if (show_progress == -1) show_progress = isatty(2); if (exclude_promisor_objects) { - fetch_if_missing = 0; + repo->fetch_if_missing = 0; revs.exclude_promisor_objects = 1; } diff --git a/builtin/repack.c b/builtin/repack.c index db504d673fcf52..c4360382c1fce2 100644 --- a/builtin/repack.c +++ b/builtin/repack.c @@ -14,6 +14,11 @@ #include "promisor-remote.h" #include "repack.h" #include "shallow.h" +#include "list-objects-filter-options.h" +#include "oidset.h" +#include "hex.h" +#include "wt-status.h" +#include "read-cache-ll.h" #define ALL_INTO_ONE 1 #define LOOSEN_UNREACHABLE 2 @@ -28,11 +33,15 @@ static int use_delta_islands; static int run_update_server_info = 1; static char *packdir, *packtmp_name, *packtmp; static int midx_must_contain_cruft = 1; +static int drop_filtered; +static int dry_run; +static int write_bitmaps_given; static const char *const git_repack_usage[] = { N_("git repack [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [-m]\n" "[--window=] [--depth=] [--threads=] [--keep-pack=]\n" - "[--write-midx[=]] [--name-hash-version=] [--path-walk]"), + "[--write-midx[=]] [--name-hash-version=] [--path-walk]\n" + "[--filter=] [--drop-filtered [--dry-run]]"), NULL }; @@ -111,6 +120,21 @@ static int repack_config(const char *var, const char *value, return git_default_config(var, value, ctx, cb); } +static int option_parse_write_bitmaps(const struct option *opt, const char *arg, + int unset) +{ + int *value = opt->value; + + BUG_ON_OPT_ARG(arg); + if (unset) + *value = 0; + else + *value = 1; + + write_bitmaps_given = 1; + return 0; +} + static int option_parse_write_midx(const struct option *opt, const char *arg, int unset) { @@ -140,6 +164,7 @@ int cmd_repack(int argc, struct string_list_item *item; struct string_list names = STRING_LIST_INIT_DUP; struct existing_packs existing = EXISTING_PACKS_INIT; + struct oidset drop_oids = OIDSET_INIT; struct pack_geometry geometry = { 0 }; struct tempfile *refs_snapshot = NULL; int i, ret; @@ -194,8 +219,9 @@ int cmd_repack(int argc, OPT__QUIET(&po_args.quiet, N_("be quiet")), OPT_BOOL('l', "local", &po_args.local, N_("pass --local to git-pack-objects")), - OPT_BOOL('b', "write-bitmap-index", &write_bitmaps, - N_("write bitmap index")), + OPT_CALLBACK_F('b', "write-bitmap-index", &write_bitmaps, NULL, + N_("write bitmap index"), + PARSE_OPT_NOARG, option_parse_write_bitmaps), OPT_BOOL('i', "delta-islands", &use_delta_islands, N_("pass --delta-islands to git-pack-objects")), OPT_STRING(0, "unpack-unreachable", &unpack_unreachable, N_("approxidate"), @@ -231,6 +257,10 @@ int cmd_repack(int argc, N_("pack prefix to store a pack containing pruned objects")), OPT_STRING(0, "filter-to", &filter_to, N_("dir"), N_("pack prefix to store a pack containing filtered out objects")), + OPT_BOOL(0, "drop-filtered", &drop_filtered, + N_("delete filtered out objects (requires --filter)")), + OPT_BOOL(0, "dry-run", &dry_run, + N_("only show which objects would be dropped")), OPT_END() }; @@ -252,6 +282,118 @@ int cmd_repack(int argc, po_args.depth = xstrdup_or_null(opt_depth); po_args.threads = xstrdup_or_null(opt_threads); + die_for_incompatible_opt2(drop_filtered, "--drop-filtered", + !!filter_to, "--filter-to"); + + if (dry_run && !drop_filtered) + die(_("--dry-run only takes effect with --drop-filtered")); + + if (drop_filtered) { + if (!po_args.filter_options.choice) + die(_("--drop-filtered requires --filter")); + + if (!(pack_everything & ALL_INTO_ONE)) + die(_("--drop-filtered requires -a")); + + /* + * Only blob:limit= is supported for now. Reject other + * filter choices early, before walking the object database. + */ + if (po_args.filter_options.choice != LOFC_BLOB_LIMIT) + die(_("--drop-filtered only supports --filter=blob:limit= for now")); + + /* + * An explicit -b on the command line is a conflict we have to + * report; a bitmap setting from config is silently overridden + * for the duration of the command. + */ + if (write_bitmaps_given && write_bitmaps > 0) + die(_("options '%s' and '%s' cannot be used together"), + "--drop-filtered", "--write-bitmap-index"); + + /* + * Without a promisor remote there is nowhere to re-fetch the + * dropped objects from, so dropping them would be permanent + * data loss. + */ + if (!repo_has_promisor_remote(repo)) + die(_("--drop-filtered requires a promisor remote")); + + /* + * Refuse to run while another operation is in progress. A + * dropped object would just be lazily re-fetched when the + * operation resumes, but triggering a network fetch in the + * middle of a half-finished + * merge/rebase/cherry-pick/revert/bisect is a poor + * experience, so this is a UX convenience rather than a + * safety measure. Bare repositories have no such state, so + * the check is skipped there. + */ + if (!is_bare_repository(repo)) { + struct wt_status_state state = { 0 }; + + wt_status_get_state(repo, &state, 0); + if (state.merge_in_progress || state.revert_in_progress || + state.rebase_in_progress || state.bisect_in_progress || + state.cherry_pick_in_progress || state.am_in_progress || + state.rebase_interactive_in_progress) { + wt_status_state_free_buffers(&state); + die(_("--drop-filtered cannot be used while " + "another operation (merge, rebase, am, " + "cherry-pick, revert, or bisect) is in " + "progress")); + } + wt_status_state_free_buffers(&state); + } + + write_bitmaps = 0; + + /* + * Dropping objects means rebuilding the promisor packs + * without them and then removing the old packs, so the + * redundant packs must be deleted. Imply -d on a real run. + */ + if (!dry_run) + delete_redundant = 1; + + ret = enumerate_promisor_blobs(repo, &po_args.filter_options, &drop_oids); + + if (ret) + goto cleanup; + + /* + * Refuse to drop blobs that the current index references. + * Such a blob would only be lazily re-fetched by the next + * command that touches the worktree, so dropping it reclaims + * nothing. This guard just avoids that churn. Bare + * repositories have no index, so the check is skipped there. + */ + if (!is_bare_repository(repo) && oidset_size(&drop_oids)) { + struct index_state *istate = repo->index; + unsigned int i; + + if (repo_read_index(repo) < 0) + die(_("could not read the index")); + + for (i = 0; i < istate->cache_nr; i++) { + const struct cache_entry *ce = istate->cache[i]; + + if (oidset_contains(&drop_oids, &ce->oid)) + die(_("cannot drop '%s' (%s): it is referenced by the current index"), + ce->name, oid_to_hex(&ce->oid)); + } + } + + if (dry_run) { + struct oidset_iter iter; + const struct object_id *oid; + + oidset_iter_init(&drop_oids, &iter); + while ((oid = oidset_iter_next(&iter))) + printf("%s\n", oid_to_hex(oid)); + } + } + if (delete_redundant && repo->repository_format_precious_objects) die(_("cannot delete packs in a precious-objects repo")); @@ -362,7 +504,8 @@ int cmd_repack(int argc, strvec_push(&cmd.args, "--delta-islands"); if (pack_everything & ALL_INTO_ONE) { - repack_promisor_objects(repo, &po_args, &names, packtmp); + repack_promisor_objects(repo, &po_args, &names, packtmp, + (drop_filtered && !dry_run) ? &drop_oids : NULL); if (existing_packs_has_non_kept(&existing) && delete_redundant && @@ -545,7 +688,7 @@ int cmd_repack(int argc, } } - if (po_args.filter_options.choice) { + if (po_args.filter_options.choice && !drop_filtered) { struct write_pack_opts opts = { .po_args = &po_args, .destination = filter_to, @@ -638,6 +781,7 @@ int cmd_repack(int argc, cleanup: string_list_clear(&keep_pack_list, 0); string_list_clear(&names, 1); + oidset_clear(&drop_oids); existing_packs_release(&existing); pack_geometry_release(&geometry); pack_objects_args_release(&po_args); diff --git a/builtin/replay.c b/builtin/replay.c index 39e3a86f6c10ab..d39626a37d055c 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -85,7 +85,7 @@ int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " "([--contained] --onto= | --advance= | --revert=)\n" - "[--ref=] [--ref-action=] "), + "[--ref=] [--ref-action=] [--linearize] "), NULL }; struct option replay_options[] = { @@ -111,6 +111,8 @@ int cmd_replay(int argc, N_("mode"), N_("control ref update behavior (update|print)"), PARSE_OPT_NONEG), + OPT_BOOL(0, "linearize", &opts.linearize, + N_("drop merge commits, replaying only non-merge commits")), OPT_END() }; @@ -132,6 +134,8 @@ int cmd_replay(int argc, opts.contained, "--contained"); die_for_incompatible_opt2(!!opts.ref, "--ref", !!opts.contained, "--contained"); + die_for_incompatible_opt2(opts.linearize, "--linearize", + !!opts.contained, "--contained"); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); diff --git a/builtin/rev-list.c b/builtin/rev-list.c index 02818b81c63fd7..6b596231ab1ab0 100644 --- a/builtin/rev-list.c +++ b/builtin/rev-list.c @@ -500,7 +500,8 @@ static void print_disk_usage(off_t size) strbuf_release(&sb); } -static inline int parse_missing_action_value(const char *value) +static inline int parse_missing_action_value(struct repository *repo, + const char *value) { if (!strcmp(value, "error")) { arg_missing_action = MA_ERROR; @@ -509,25 +510,25 @@ static inline int parse_missing_action_value(const char *value) if (!strcmp(value, "allow-any")) { arg_missing_action = MA_ALLOW_ANY; - fetch_if_missing = 0; + repo->fetch_if_missing = 0; return 1; } if (!strcmp(value, "print")) { arg_missing_action = MA_PRINT; - fetch_if_missing = 0; + repo->fetch_if_missing = 0; return 1; } if (!strcmp(value, "print-info")) { arg_missing_action = MA_PRINT_INFO; - fetch_if_missing = 0; + repo->fetch_if_missing = 0; return 1; } if (!strcmp(value, "allow-promisor")) { arg_missing_action = MA_ALLOW_PROMISOR; - fetch_if_missing = 0; + repo->fetch_if_missing = 0; return 1; } @@ -692,7 +693,7 @@ static void prepare_maximal_independent(struct rev_info *revs) int cmd_rev_list(int argc, const char **argv, const char *prefix, - struct repository *repo UNUSED) + struct repository *repo) { struct rev_info revs; struct rev_list_info info; @@ -745,10 +746,10 @@ int cmd_rev_list(int argc, for (i = 1; i < argc; i++) { const char *arg = argv[i]; if (!strcmp(arg, "--exclude-promisor-objects")) { - fetch_if_missing = 0; + repo->fetch_if_missing = 0; revs.exclude_promisor_objects = 1; } else if (skip_prefix(arg, "--missing=", &arg)) { - parse_missing_action_value(arg); + parse_missing_action_value(repo, arg); } else if (!strcmp(arg, "-z")) { line_term = '\0'; info_term = '\0'; diff --git a/builtin/unpack-objects.c b/builtin/unpack-objects.c index 4263edfbecdd39..3392a3b87ddb0c 100644 --- a/builtin/unpack-objects.c +++ b/builtin/unpack-objects.c @@ -358,51 +358,55 @@ static void unpack_non_delta_entry(enum object_type type, unsigned long size, write_object(nr, type, buf, size); } -struct input_zstream_data { +struct zlib_stream { + struct odb_stream base; git_zstream *zstream; int status; }; -static ssize_t feed_input_zstream(struct odb_write_stream *in_stream, - unsigned char *buf, size_t buf_len) +static ssize_t zlib_stream_read(struct odb_stream *in_stream, + char *buf, size_t buf_len) { - struct input_zstream_data *data = in_stream->data; + struct zlib_stream *data = container_of(in_stream, struct zlib_stream, base); git_zstream *zstream = data->zstream; - void *in = fill(1); - if (in_stream->is_finished) + if (data->status != Z_OK) return 0; - zstream->next_out = buf; + zstream->next_out = (unsigned char *) buf; zstream->avail_out = buf_len; - zstream->next_in = in; - zstream->avail_in = len; - data->status = git_inflate(zstream, 0); + while (data->status == Z_OK && zstream->avail_out == buf_len) { + zstream->next_in = fill(1); + zstream->avail_in = len; + data->status = git_inflate(zstream, 0); + use(len - zstream->avail_in); + } - in_stream->is_finished = data->status != Z_OK; - use(len - zstream->avail_in); return buf_len - zstream->avail_out; } static void stream_blob(unsigned long size, unsigned nr) { git_zstream zstream = { 0 }; - struct input_zstream_data data = { 0 }; - struct odb_write_stream in_stream = { - .read = feed_input_zstream, - .data = &data, + struct zlib_stream in_stream = { + .base = { + .read = zlib_stream_read, + .size = size, + .type = OBJ_BLOB, + }, + .zstream = &zstream, + .status = Z_OK, }; struct obj_info *info = &obj_list[nr]; - data.zstream = &zstream; git_inflate_init(&zstream); - if (odb_write_object_stream(the_repository->objects, &in_stream, size, &info->oid)) + if (odb_write_object_stream(the_repository->objects, &in_stream.base, &info->oid)) die(_("failed to write object in stream")); - if (data.status != Z_STREAM_END) - die(_("inflate returned (%d)"), data.status); + if (in_stream.status != Z_STREAM_END) + die(_("inflate returned (%d)"), in_stream.status); git_inflate_end(&zstream); if (strict) { diff --git a/chdir-notify.c b/chdir-notify.c index 1237a45e2e6492..55773c24c96eda 100644 --- a/chdir-notify.c +++ b/chdir-notify.c @@ -7,25 +7,22 @@ #include "trace.h" struct chdir_notify_entry { - const char *name; chdir_notify_callback cb; void *data; struct list_head list; }; static LIST_HEAD(chdir_notify_entries); -void chdir_notify_register(const char *name, - chdir_notify_callback cb, +void chdir_notify_register(chdir_notify_callback cb, void *data) { struct chdir_notify_entry *e = xmalloc(sizeof(*e)); - e->name = name; e->cb = cb; e->data = data; list_add_tail(&e->list, &chdir_notify_entries); } -void chdir_notify_unregister(const char *name, chdir_notify_callback cb, +void chdir_notify_unregister(chdir_notify_callback cb, void *data) { struct list_head *pos, *p; @@ -34,8 +31,7 @@ void chdir_notify_unregister(const char *name, chdir_notify_callback cb, struct chdir_notify_entry *e = list_entry(pos, struct chdir_notify_entry, list); - if (e->cb != cb || e->data != data || !e->name != !name || - (e->name && strcmp(e->name, name))) + if (e->cb != cb || e->data != data) continue; list_del(pos); @@ -64,7 +60,7 @@ int chdir_notify(const char *new_cwd) list_for_each(pos, &chdir_notify_entries) { struct chdir_notify_entry *e = list_entry(pos, struct chdir_notify_entry, list); - e->cb(e->name, old_cwd.buf, new_cwd, e->data); + e->cb(old_cwd.buf, new_cwd, e->data); } strbuf_release(&old_cwd); diff --git a/chdir-notify.h b/chdir-notify.h index 36b4114472e31d..e4ae38e12dd8b9 100644 --- a/chdir-notify.h +++ b/chdir-notify.h @@ -33,13 +33,11 @@ * $GIT_TRACE_SETUP. It may be NULL, but if non-NULL should point to * storage which lasts as long as the registration is active. */ -typedef void (*chdir_notify_callback)(const char *name, - const char *old_cwd, +typedef void (*chdir_notify_callback)(const char *old_cwd, const char *new_cwd, void *data); -void chdir_notify_register(const char *name, chdir_notify_callback cb, void *data); -void chdir_notify_unregister(const char *name, chdir_notify_callback cb, - void *data); +void chdir_notify_register(chdir_notify_callback cb, void *data); +void chdir_notify_unregister(chdir_notify_callback cb, void *data); /* * diff --git a/commit-reach.c b/commit-reach.c index b53c6b1cdff3f3..b16eea2355356f 100644 --- a/commit-reach.c +++ b/commit-reach.c @@ -11,6 +11,7 @@ #include "tag.h" #include "commit-reach.h" #include "ewah/ewok.h" +#include "trace2.h" /* Remember to update object flag allocation in object.h */ #define PARENT1 (1u<<16) @@ -78,25 +79,111 @@ static void clear_nonstale_queue(struct nonstale_queue *queue) queue->max_nonstale = NULL; } -static void nonstale_queue_put_dedup(struct nonstale_queue *queue, - struct commit *c) +/* + * Priority queue with per-side commit counters for paint_down_to_common(). + * Each non-stale queued commit occupies exactly one bucket: PARENT1-only, + * PARENT2-only, or both (a pending merge-base candidate). + */ +struct paint_state { + struct prio_queue queue; + size_t parent1_count; + size_t parent2_count; + size_t mb_candidate_count; + timestamp_t min_generation; + timestamp_t last_gen; + timestamp_t topo_ceiling; +}; + +static void paint_count_update(struct paint_state *state, + unsigned flags, int delta) { - if (c->object.flags & ENQUEUED) - return; - c->object.flags |= ENQUEUED; - nonstale_queue_put(queue, c); + switch (flags & (PARENT1 | PARENT2 | STALE)) { + case PARENT1: + state->parent1_count += delta; + break; + + case PARENT2: + state->parent2_count += delta; + break; + + case PARENT1 | PARENT2: + state->mb_candidate_count += delta; + break; + + case PARENT1 | PARENT2 | STALE: + break; + + default: + BUG("unexpected paint state"); + } } -static struct commit *nonstale_queue_get_dedup(struct nonstale_queue *queue) +static void paint_queue_put(struct paint_state *state, + struct commit *c, unsigned add_flags) { - struct commit *commit = nonstale_queue_get(queue); + unsigned old_flags = c->object.flags; + c->object.flags |= add_flags; + + if (old_flags & ENQUEUED) { + paint_count_update(state, old_flags, -1); + paint_count_update(state, c->object.flags, 1); + } else { + c->object.flags |= ENQUEUED; + prio_queue_put(&state->queue, c); + paint_count_update(state, c->object.flags, 1); + } +} + +/* + * Dequeue the next commit for the paint walk, or return NULL when + * no more merge bases can be discovered. + */ +static struct commit *paint_queue_get(struct paint_state *state) +{ + struct commit *commit = prio_queue_get(&state->queue); + timestamp_t generation; + + if (!commit) + return NULL; + + commit->object.flags &= ~ENQUEUED; + generation = commit_graph_generation(commit); - if (commit) - commit->object.flags &= ~ENQUEUED; + if (state->min_generation && generation > state->last_gen) + BUG("bad generation skip %"PRItime" > %"PRItime" at %s", + generation, state->last_gen, + oid_to_hex(&commit->object.oid)); + state->last_gen = generation; + + /* generation cutoff */ + if (generation < state->min_generation) + return NULL; + + /* + * Check exit condition before decrementing: the counters + * still include this commit, so the last non-stale commit + * sees a non-zero count and is returned for processing. + */ + if (!state->mb_candidate_count) { + /* only stale entries remain */ + if (!state->parent1_count && !state->parent2_count) + return NULL; + + /* one side is exhausted */ + if ((!state->parent1_count || !state->parent2_count) && + generation < state->topo_ceiling) + return NULL; + } + + paint_count_update(state, commit->object.flags, -1); return commit; } -/* all input commits in one and twos[] must have been parsed! */ +/* + * See Documentation/technical/paint-down-to-common.adoc + * + * All input commits in one and twos[] must have been parsed! + */ static int paint_down_to_common(struct repository *r, struct commit *one, int n, struct commit **twos, @@ -104,45 +191,40 @@ static int paint_down_to_common(struct repository *r, enum merge_base_flags mb_flags, struct commit_list **result) { - struct nonstale_queue queue = { - { compare_commits_by_gen_then_commit_date } + /* + * Generation ordering is required for the side-exhaustion and + * single-result early exits, which rely on topological traversal + * order (children visited before parents) in the ordered region. + */ + struct paint_state state = { + .queue = { compare_commits_by_gen_then_commit_date } }; + struct commit *commit; int i; - int gen_ordered = 1; - timestamp_t last_gen = GENERATION_NUMBER_INFINITY; + int steps = 0; struct commit_list **tail = result; - if (!min_generation && !corrected_commit_dates_enabled(r)) { - queue.pq.compare = compare_commits_by_commit_date; - gen_ordered = 0; - } + state.min_generation = min_generation; + state.last_gen = GENERATION_NUMBER_INFINITY; + state.topo_ceiling = corrected_commit_dates_enabled(r) + ? GENERATION_NUMBER_INFINITY + : GENERATION_NUMBER_V1_MAX; + one->object.flags |= PARENT1; if (!n) { commit_list_append(one, result); return 0; } - nonstale_queue_put_dedup(&queue, one); + paint_queue_put(&state, one, 0); - for (i = 0; i < n; i++) { - twos[i]->object.flags |= PARENT2; - nonstale_queue_put_dedup(&queue, twos[i]); - } + for (i = 0; i < n; i++) + paint_queue_put(&state, twos[i], PARENT2); - while (queue.max_nonstale) { - struct commit *commit = nonstale_queue_get_dedup(&queue); + while ((commit = paint_queue_get(&state))) { struct commit_list *parents; int flags; - timestamp_t generation = commit_graph_generation(commit); - - if (min_generation && generation > last_gen) - BUG("bad generation skip %"PRItime" > %"PRItime" at %s", - generation, last_gen, - oid_to_hex(&commit->object.oid)); - last_gen = generation; - - if (generation < min_generation) - break; + steps++; flags = commit->object.flags & (PARENT1 | PARENT2 | STALE); if (flags == (PARENT1 | PARENT2)) { @@ -155,8 +237,7 @@ static int paint_down_to_common(struct repository *r, * descendant of this one. */ if (!(mb_flags & MERGE_BASE_FIND_ALL) && - gen_ordered && - generation < GENERATION_NUMBER_INFINITY) + state.last_gen < state.topo_ceiling) break; } /* Mark parents of a found merge stale */ @@ -169,7 +250,7 @@ static int paint_down_to_common(struct repository *r, if ((p->object.flags & flags) == flags) continue; if (repo_parse_commit(r, p)) { - clear_nonstale_queue(&queue); + clear_prio_queue(&state.queue); commit_list_free(*result); *result = NULL; /* @@ -184,12 +265,13 @@ static int paint_down_to_common(struct repository *r, return error(_("could not parse commit %s"), oid_to_hex(&p->object.oid)); } - p->object.flags |= flags; - nonstale_queue_put_dedup(&queue, p); + paint_queue_put(&state, p, flags); } } - clear_nonstale_queue(&queue); + clear_prio_queue(&state.queue); + trace2_data_intmax("paint_down_to_common", r, + "steps", steps); commit_list_sort_by_date(result); return 0; } diff --git a/common-init.c b/common-init.c index d26c9c1f20239e..4a3fa4d7bee3be 100644 --- a/common-init.c +++ b/common-init.c @@ -47,7 +47,7 @@ static void setup_environment(void) update_ref_namespace(NAMESPACE_REPLACE, git_replace_ref_base); if (git_env_bool(NO_LAZY_FETCH_ENVIRONMENT, 0)) - fetch_if_missing = 0; + the_repository->fetch_if_missing = 0; } void init_git(const char **argv) diff --git a/compat/darwin.h b/compat/darwin.h new file mode 100644 index 00000000000000..6fbdc345608ec4 --- /dev/null +++ b/compat/darwin.h @@ -0,0 +1,8 @@ +#ifndef COMPAT_DARWIN_H +#define COMPAT_DARWIN_H + +int darwin_regexec_buf(const regex_t *preg, const char *buf, size_t size, + size_t nmatch, regmatch_t pmatch[], int eflags); +#define regexec_buf darwin_regexec_buf + +#endif diff --git a/compat/darwin/regexec.c b/compat/darwin/regexec.c new file mode 100644 index 00000000000000..13fb7d5946bc8c --- /dev/null +++ b/compat/darwin/regexec.c @@ -0,0 +1,91 @@ +#include "git-compat-util.h" + +#include + +/* + * Darwin's TRE regex engine leaks an internal buffer when it encounters an + * invalid multibyte sequence. Since the leak has already happened when + * regexec() reports REG_ILLSEQ, keep invalid bytes out of regexec() by + * searching each valid segment separately. + */ + +/* + * Search buf[start, end), where size is the full size of buf. REG_STARTEND + * keeps match offsets relative to buf. Do not let an internal segment create + * a false beginning or end of line. + */ +static int regexec_segment(const regex_t *preg, const char *buf, + size_t size, size_t start, size_t end, + size_t nmatch, regmatch_t pmatch[], int eflags) +{ + eflags |= REG_STARTEND; + if (start > 0) + eflags |= REG_NOTBOL; + if (end < size) + eflags |= REG_NOTEOL; + pmatch[0].rm_so = start; + pmatch[0].rm_eo = end; + return regexec(preg, buf, nmatch, pmatch, eflags); +} + +int darwin_regexec_buf(const regex_t *preg, const char *buf, size_t size, + size_t nmatch, regmatch_t pmatch[], int eflags) +{ + size_t seg_start = 0, i = 0; + mbstate_t mbs; + + assert(nmatch > 0 && pmatch); + + /* + * A single-byte locale cannot contain an invalid multibyte sequence, + * so use regexec() directly. + */ + if (MB_CUR_MAX == 1) { + pmatch[0].rm_so = 0; + pmatch[0].rm_eo = size; + return regexec(preg, buf, nmatch, pmatch, eflags | REG_STARTEND); + } + + memset(&mbs, 0, sizeof(mbs)); + while (i < size) { + unsigned char c = (unsigned char)buf[i]; + size_t n; + + if (c < 0x80) { + i++; + continue; + } + + n = mbrtowc(NULL, buf + i, size - i, &mbs); + if (!n) + n = 1; + if (n != (size_t)-1 && n != (size_t)-2) { + i += n; + continue; + } + + /* + * -1 denotes an encoding error; -2 denotes an incomplete + * trailing sequence. In either case, buf[i] cannot begin a + * complete valid character within this buffer. Search an + * empty initial segment to preserve zero-width matches at the + * true beginning. + */ + if (i > seg_start || i == 0) { + int ret = regexec_segment(preg, buf, size, seg_start, i, + nmatch, pmatch, eflags); + if (ret != REG_NOMATCH) + return ret; + } + i++; + seg_start = i; + memset(&mbs, 0, sizeof(mbs)); + } + + /* + * Search the final segment even when it is empty, so an empty buffer + * or a buffer ending in invalid bytes still has its true end. + */ + return regexec_segment(preg, buf, size, seg_start, size, + nmatch, pmatch, eflags); +} diff --git a/compat/mingw.c b/compat/mingw.c index 4c2f26d4548147..0c8179f3458bb8 100644 --- a/compat/mingw.c +++ b/compat/mingw.c @@ -3157,6 +3157,45 @@ int xwcstoutf(char *utf, const wchar_t *wcs, size_t utflen) return -1; } +#ifdef ENSURE_MSYSTEM_IS_SET +#if !defined(RUNTIME_PREFIX) || !defined(HAVE_WPGMPTR) || !defined(MINGW_PREFIX) +static size_t append_system_bin_dirs(char *path UNUSED, size_t size UNUSED) +{ + return 0; +} +#else +static size_t append_system_bin_dirs(char *path, size_t size) +{ + char prefix[32768]; + const char *slash; + size_t len = xwcstoutf(prefix, _wpgmptr, sizeof(prefix)), off = 0; + + if (len == 0 || len >= sizeof(prefix) || + !(slash = find_last_dir_sep(prefix))) + return 0; + /* strip trailing `git.exe` */ + len = slash - prefix; + + /* strip trailing `cmd` or `\bin` or `bin` or `libexec\git-core` */ + if (strip_suffix_mem(prefix, &len, "\\" MINGW_PREFIX "\\libexec\\git-core") || + strip_suffix_mem(prefix, &len, "\\" MINGW_PREFIX "\\bin")) + off += xsnprintf(path + off, size - off, + "%.*s\\" MINGW_PREFIX "\\bin;", (int)len, prefix); + else if (strip_suffix_mem(prefix, &len, "\\cmd") || + strip_suffix_mem(prefix, &len, "\\bin") || + strip_suffix_mem(prefix, &len, "\\libexec\\git-core")) + off += xsnprintf(path + off, size - off, + "%.*s\\" MINGW_PREFIX "\\bin;", (int)len, prefix); + else + return 0; + + off += xsnprintf(path + off, size - off, + "%.*s\\usr\\bin;", (int)len, prefix); + return off; +} +#endif +#endif + static void setup_windows_environment(void) { char *tmp = getenv("TMPDIR"); @@ -3208,6 +3247,35 @@ static void setup_windows_environment(void) if (!tmp && (tmp = getenv("USERPROFILE"))) setenv("HOME", tmp, 1); } + + if (!getenv("PLINK_PROTOCOL")) + setenv("PLINK_PROTOCOL", "ssh", 0); + +#ifdef ENSURE_MSYSTEM_IS_SET + if (!(tmp = getenv("MSYSTEM")) || !tmp[0]) { + const char *home = getenv("HOME"), *path = getenv("PATH"); + char buf[32768]; + size_t off = 0; + + setenv("MSYSTEM", ENSURE_MSYSTEM_IS_SET, 1); + + if (home) + off += xsnprintf(buf + off, sizeof(buf) - off, + "%s\\bin;", home); + off += append_system_bin_dirs(buf + off, sizeof(buf) - off); + if (path) + off += xsnprintf(buf + off, sizeof(buf) - off, + "%s", path); + else if (off > 0) + buf[off - 1] = '\0'; + else + buf[0] = '\0'; + setenv("PATH", buf, 1); + } +#endif + + if (!getenv("LC_ALL") && !getenv("LC_CTYPE") && !getenv("LANG")) + setenv("LC_CTYPE", "C.UTF-8", 1); } int mingw_platform_has_symlinks(void) diff --git a/compat/posix.h b/compat/posix.h index e2e794cad7d419..71cc7316204187 100644 --- a/compat/posix.h +++ b/compat/posix.h @@ -148,6 +148,9 @@ #include #include #include +#ifndef NO_WRITEV +#include +#endif #include #ifndef NO_SYS_SELECT_H #include @@ -334,6 +337,17 @@ int git_lstat(const char *, struct stat *); ssize_t git_pread(int fd, void *buf, size_t count, off_t offset); #endif +#ifdef NO_WRITEV +#define writev git_writev +#define iovec git_iovec +struct git_iovec { + void *iov_base; + size_t iov_len; +}; + +ssize_t git_writev(int fd, const struct iovec *iov, int iovcnt); +#endif + #ifdef NO_SETENV #define setenv gitsetenv int gitsetenv(const char *, const char *, int); diff --git a/compat/pread.c b/compat/pread.c index 484e6d4c716ef6..ac7d058cb895db 100644 --- a/compat/pread.c +++ b/compat/pread.c @@ -7,6 +7,8 @@ ssize_t git_pread(int fd, void *buf, size_t count, off_t offset) ssize_t rc; current_offset = lseek(fd, 0, SEEK_CUR); + if (current_offset < 0) + return -1; if (lseek(fd, offset, SEEK_SET) < 0) return -1; diff --git a/compat/writev.c b/compat/writev.c new file mode 100644 index 00000000000000..540f66de61fb09 --- /dev/null +++ b/compat/writev.c @@ -0,0 +1,41 @@ +#include "../git-compat-util.h" +#include "../wrapper.h" + +ssize_t git_writev(int fd, const struct iovec *iov, int iovcnt) +{ + size_t sum = 0; + + if (iovcnt <= 0) { + errno = EINVAL; + return -1; + } + + /* + * According to writev(3p), the syscall shall error with EINVAL in case + * the sum of `iov_len` overflows `ssize_t`. + */ + for (int i = 0; i < iovcnt; i++) { + if (iov[i].iov_len > maximum_signed_value_of_type(ssize_t) || + unsigned_add_overflows(iov[i].iov_len, sum) || + iov[i].iov_len + sum > maximum_signed_value_of_type(ssize_t)) { + errno = EINVAL; + return -1; + } + + sum += iov[i].iov_len; + } + + /* + * We only ever write the first non-empty vector so that we can + * guarantee the call to be non-interleaving as guaranteed by POSIX. + * This works just fine as callers have to loop around writev anyway. + */ + for (int i = 0; i < iovcnt; i++) { + if (!iov[i].iov_len) + continue; + return xwrite(fd, iov[i].iov_base, iov[i].iov_len); + } + + /* When all iovec members were zero we ought to return 0 according to POSIX. */ + return 0; +} diff --git a/config.c b/config.c index 1bdd702e7a3969..d9019e7e6c34b0 100644 --- a/config.c +++ b/config.c @@ -1271,6 +1271,15 @@ ssize_t git_config_ssize_t(const char *name, const char *value, return ret; } +size_t git_config_size_t(const char *name, const char *value, + const struct key_value_info *kvi) +{ + size_t ret; + if (!git_parse_size_t(value, &ret)) + die_bad_number(name, value, kvi); + return ret; +} + double git_config_double(const char *name, const char *value, const struct key_value_info *kvi) { diff --git a/config.h b/config.h index 31fe3e29611e11..b66dd08007c97a 100644 --- a/config.h +++ b/config.h @@ -282,6 +282,9 @@ unsigned long git_config_ulong(const char *, const char *, ssize_t git_config_ssize_t(const char *, const char *, const struct key_value_info *); +size_t git_config_size_t(const char *, const char *, + const struct key_value_info *); + /** * Identically to `git_config_double`, but for double-precision floating point * values. diff --git a/config.mak.uname b/config.mak.uname index 9ebd240378ca59..d28ec12a2575aa 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -154,6 +154,7 @@ ifeq ($(uname_S),Darwin) HAVE_DEV_TTY = YesPlease COMPAT_OBJS += compat/precompose_utf8.o BASIC_CFLAGS += -DPRECOMPOSE_UNICODE + DARWIN_REGEXEC = YesPlease BASIC_CFLAGS += -DPROTECT_HFS_DEFAULT=1 HAVE_BSD_SYSCTL = YesPlease FREAD_READS_DIRECTORIES = UnfortunatelyYes @@ -465,14 +466,8 @@ ifeq ($(uname_S),Windows) GIT_VERSION := $(GIT_VERSION).MSVC pathsep = ; # Assume that this is built in Git for Windows' SDK - ifeq (MINGW32,$(MSYSTEM)) - prefix = /mingw32 - else - ifeq (CLANGARM64,$(MSYSTEM)) - prefix = /clangarm64 - else - prefix = /mingw64 - endif + ifneq (,$(MSYSTEM)) + prefix = $(MINGW_PREFIX) endif # Prepend MSVC 64-bit tool-chain to PATH. # @@ -483,6 +478,7 @@ ifeq ($(uname_S),Windows) SANE_TOOL_PATH ?= $(msvc_bin_dir_msys) HAVE_ALLOCA_H = YesPlease NO_PREAD = YesPlease + NO_WRITEV = YesPlease NEEDS_CRYPTO_WITH_SSL = YesPlease NO_LIBGEN_H = YesPlease NO_POLL = YesPlease @@ -524,7 +520,8 @@ ifeq ($(uname_S),Windows) NO_POSIX_GOODIES = UnfortunatelyYes NATIVE_CRLF = YesPlease DEFAULT_HELP_FORMAT = html -ifeq (/mingw64,$(subst 32,64,$(subst clangarm,mingw,$(prefix)))) + SKIP_DASHED_BUILT_INS = YabbaDabbaDoo +ifneq (,$(MINGW_PREFIX)) # Move system config into top-level /etc/ ETC_GITCONFIG = ../etc/gitconfig ETC_GITATTRIBUTES = ../etc/gitattributes @@ -540,7 +537,9 @@ endif compat/win32/pthread.o compat/win32/syslog.o \ compat/win32/trace2_win32_process_info.o \ compat/win32/dirent.o - COMPAT_CFLAGS = -D__USE_MINGW_ACCESS -DDETECT_MSYS_TTY -DNOGDI -DHAVE_STRING_H -Icompat -Icompat/regex -Icompat/win32 -DSTRIP_EXTENSION=\".exe\" + COMPAT_CFLAGS = -D__USE_MINGW_ACCESS -DDETECT_MSYS_TTY \ + -DENSURE_MSYSTEM_IS_SET="\"$(MSYSTEM)\"" -DMINGW_PREFIX="\"$(patsubst /%,%,$(MINGW_PREFIX))\"" \ + -DNOGDI -DHAVE_STRING_H -Icompat -Icompat/regex -Icompat/win32 -DSTRIP_EXTENSION=\".exe\" BASIC_LDFLAGS = -IGNORE:4217 -IGNORE:4049 -NOLOGO -ENTRY:wmainCRTStartup -SUBSYSTEM:CONSOLE # invalidcontinue.obj allows Git's source code to close the same file # handle twice, or to access the osfhandle of an already-closed stdout @@ -697,6 +696,7 @@ ifeq ($(uname_S),MINGW) pathsep = ; HAVE_ALLOCA_H = YesPlease NO_PREAD = YesPlease + NO_WRITEV = YesPlease NEEDS_CRYPTO_WITH_SSL = YesPlease NO_LIBGEN_H = YesPlease NO_POLL = YesPlease @@ -716,6 +716,7 @@ ifeq ($(uname_S),MINGW) FSMONITOR_DAEMON_BACKEND = win32 FSMONITOR_OS_SETTINGS = win32 + SKIP_DASHED_BUILT_INS = YabbaDabbaDoo RUNTIME_PREFIX = YesPlease HAVE_WPGMPTR = YesWeDo NO_ST_BLOCKS_IN_STRUCT_STAT = YesPlease @@ -754,26 +755,25 @@ ifeq ($(uname_S),MINGW) ifneq (,$(findstring -O,$(filter-out -O0 -Og,$(CFLAGS)))) BASIC_LDFLAGS += -Wl,--dynamicbase endif - ifeq (MINGW32,$(MSYSTEM)) - prefix = /mingw32 - HOST_CPU = i686 - BASIC_LDFLAGS += -Wl,--pic-executable,-e,_mainCRTStartup - endif - ifeq (MINGW64,$(MSYSTEM)) - prefix = /mingw64 - HOST_CPU = x86_64 - BASIC_LDFLAGS += -Wl,--pic-executable,-e,mainCRTStartup - else ifeq (CLANGARM64,$(MSYSTEM)) - prefix = /clangarm64 - HOST_CPU = aarch64 - BASIC_LDFLAGS += -Wl,--pic-executable,-e,mainCRTStartup - else - COMPAT_CFLAGS += -D_USE_32BIT_TIME_T - BASIC_LDFLAGS += -Wl,--large-address-aware + ifneq (,$(MSYSTEM)) + ifeq ($(MINGW_PREFIX),$(filter-out /%,$(MINGW_PREFIX))) + # Override if empty or does not start with a slash + MINGW_PREFIX := /$(shell echo '$(MSYSTEM)' | tr A-Z a-z) + endif + prefix = $(MINGW_PREFIX) + HOST_CPU = $(patsubst %-w64-mingw32,%,$(MINGW_CHOST)) + BASIC_LDFLAGS += -Wl,--pic-executable + COMPAT_CFLAGS += -DDETECT_MSYS_TTY \ + -DENSURE_MSYSTEM_IS_SET="\"$(MSYSTEM)\"" \ + -DMINGW_PREFIX="\"$(patsubst /%,%,$(MINGW_PREFIX))\"" + ifeq (MINGW32,$(MSYSTEM)) + BASIC_LDFLAGS += -Wl,--large-address-aware + endif + # Move system config into top-level /etc/ + ETC_GITCONFIG = ../etc/gitconfig + ETC_GITATTRIBUTES = ../etc/gitattributes endif - CC = gcc - COMPAT_CFLAGS += -D__USE_MINGW_ANSI_STDIO=0 -DDETECT_MSYS_TTY \ - -fstack-protector-strong + COMPAT_CFLAGS += -D__USE_MINGW_ANSI_STDIO=0 -fstack-protector-strong EXTLIBS += -lntdll EXTRA_PROGRAMS += headless-git$X INSTALL = /bin/install @@ -781,11 +781,7 @@ ifeq ($(uname_S),MINGW) HAVE_LIBCHARSET_H = YesPlease USE_GETTEXT_SCHEME = fallthrough USE_LIBPCRE = YesPlease - ifeq (/mingw64,$(subst 32,64,$(subst clangarm,mingw,$(prefix)))) - # Move system config into top-level /etc/ - ETC_GITCONFIG = ../etc/gitconfig - ETC_GITATTRIBUTES = ../etc/gitattributes - endif + NO_PYTHON = endif ifeq ($(uname_S),QNX) COMPAT_CFLAGS += -DSA_RESTART=0 diff --git a/contrib/buildsystems/CMakeLists.txt b/contrib/buildsystems/CMakeLists.txt index a57c4b464fa456..1af245c15a89d2 100644 --- a/contrib/buildsystems/CMakeLists.txt +++ b/contrib/buildsystems/CMakeLists.txt @@ -256,7 +256,14 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows") _CONSOLE DETECT_MSYS_TTY STRIP_EXTENSION=".exe" NO_SYMLINK_HEAD UNRELIABLE_FSTAT NOGDI OBJECT_CREATION_MODE=1 __USE_MINGW_ANSI_STDIO=0 OVERRIDE_STRDUP MMAP_PREVENTS_DELETE USE_WIN32_MMAP - HAVE_WPGMPTR ENSURE_MSYSTEM_IS_SET HAVE_RTLGENRANDOM) + HAVE_WPGMPTR HAVE_RTLGENRANDOM) + if(CMAKE_GENERATOR_PLATFORM STREQUAL "x64") + add_compile_definitions(ENSURE_MSYSTEM_IS_SET="MINGW64" MINGW_PREFIX="mingw64") + elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "arm64") + add_compile_definitions(ENSURE_MSYSTEM_IS_SET="CLANGARM64" MINGW_PREFIX="clangarm64") + elseif(CMAKE_GENERATOR_PLATFORM STREQUAL "x86") + add_compile_definitions(ENSURE_MSYSTEM_IS_SET="MINGW32" MINGW_PREFIX="mingw32") + endif() list(APPEND compat_SOURCES compat/mingw.c compat/winansi.c @@ -378,7 +385,7 @@ endif() #function checks set(function_checks strcasestr memmem strlcpy strtoimax strtoumax strtoull - setenv mkdtemp poll pread memmem) + setenv mkdtemp poll pread memmem writev) #unsetenv,hstrerror are incompatible with windows build if(NOT WIN32) @@ -423,6 +430,10 @@ if(NOT HAVE_MEMMEM) list(APPEND compat_SOURCES compat/memmem.c) endif() +if(NOT HAVE_WRITEV) + list(APPEND compat_SOURCES compat/writev.c) +endif() + if(NOT WIN32) if(NOT HAVE_UNSETENV) list(APPEND compat_SOURCES compat/unsetenv.c) @@ -519,6 +530,9 @@ if(NOT HAVE_REGEX) include_directories(${CMAKE_SOURCE_DIR}/compat/regex) list(APPEND compat_SOURCES compat/regex/regex.c ) add_compile_definitions(NO_REGEX NO_MBSUPPORT GAWK) +elseif(APPLE) + list(APPEND compat_SOURCES compat/darwin/regexec.c) + add_compile_definitions(DARWIN_REGEXEC) endif() diff --git a/contrib/completion/git-completion.bash b/contrib/completion/git-completion.bash index e8757877104eb9..e6dce62d3c3fb4 100644 --- a/contrib/completion/git-completion.bash +++ b/contrib/completion/git-completion.bash @@ -1735,49 +1735,59 @@ __git_checkout_default_dwim_mode () _git_checkout () { - __git_has_doubledash && return + if ! __git_has_doubledash; then + local dwim_opt="$(__git_checkout_default_dwim_mode)" - local dwim_opt="$(__git_checkout_default_dwim_mode)" + case "$prev" in + -b|-B|--orphan) + # Complete local branches (and DWIM branch + # remote branch names) for an option argument + # specifying a new branch name. This is for + # convenience, assuming new branches are + # possibly based on pre-existing branch names. + __git_complete_refs $dwim_opt --mode="heads" + return + ;; + *) + ;; + esac - case "$prev" in - -b|-B|--orphan) - # Complete local branches (and DWIM branch - # remote branch names) for an option argument - # specifying a new branch name. This is for - # convenience, assuming new branches are - # possibly based on pre-existing branch names. - __git_complete_refs $dwim_opt --mode="heads" - return - ;; - *) - ;; - esac + case "$cur" in + --conflict=*) + __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" + return + ;; + --*) + __gitcomp_builtin checkout + return + ;; + *) + # At this point, we've already handled special completion for + # the arguments to -b/-B, and --orphan. There are 3 main + # things left we can possibly complete: + # 1) a start-point for -b/-B, -d/--detach, or --orphan + # 2) a remote head, for --track + # 3) an arbitrary reference, possibly including DWIM names + # + + if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan")" ]; then + __git_complete_refs --mode="refs" + elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then + __git_complete_refs --mode="remote-heads" + else + __git_complete_refs $dwim_opt --mode="refs" + fi + ;; + esac + fi - case "$cur" in - --conflict=*) - __gitcomp "diff3 merge zdiff3" "" "${cur##--conflict=}" - ;; - --*) - __gitcomp_builtin checkout - ;; - *) - # At this point, we've already handled special completion for - # the arguments to -b/-B, and --orphan. There are 3 main - # things left we can possibly complete: - # 1) a start-point for -b/-B, -d/--detach, or --orphan - # 2) a remote head, for --track - # 3) an arbitrary reference, possibly including DWIM names - # + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "" + fi - if [ -n "$(__git_find_on_cmdline "-b -B -d --detach --orphan")" ]; then - __git_complete_refs --mode="refs" - elif [ -n "$(__git_find_on_cmdline "-t --track")" ]; then - __git_complete_refs --mode="remote-heads" - else - __git_complete_refs $dwim_opt --mode="refs" - fi - ;; - esac + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "--others --directory" + fi } __git_sequencer_inprogress_options="--continue --quit --abort --skip" @@ -1947,35 +1957,48 @@ __git_diff_difftool_options="--cached --staged _git_diff () { - __git_has_doubledash && return + if ! __git_has_doubledash; then + case "$cur" in + --diff-algorithm=*) + __gitcomp "$__git_diff_algorithms" \ + "" "${cur##--diff-algorithm=}" + return + ;; + --submodule=*) + __gitcomp "$__git_diff_submodule_formats" \ + "" "${cur##--submodule=}" + return + ;; + --color-moved=*) + __gitcomp "$__git_color_moved_opts" \ + "" "${cur##--color-moved=}" + return + ;; + --color-moved-ws=*) + __gitcomp "$__git_color_moved_ws_opts" \ + "" "${cur##--color-moved-ws=}" + return + ;; + --ws-error-highlight=*) + __gitcomp "$__git_ws_error_highlight_opts" \ + "" "${cur##--ws-error-highlight=}" + return + ;; + --*) + __gitcomp "$__git_diff_difftool_options" + return + ;; + esac + __git_complete_revlist_file + fi - case "$cur" in - --diff-algorithm=*) - __gitcomp "$__git_diff_algorithms" "" "${cur##--diff-algorithm=}" - return - ;; - --submodule=*) - __gitcomp "$__git_diff_submodule_formats" "" "${cur##--submodule=}" - return - ;; - --color-moved=*) - __gitcomp "$__git_color_moved_opts" "" "${cur##--color-moved=}" - return - ;; - --color-moved-ws=*) - __gitcomp "$__git_color_moved_ws_opts" "" "${cur##--color-moved-ws=}" - return - ;; - --ws-error-highlight=*) - __gitcomp "$__git_ws_error_highlight_opts" "" "${cur##--ws-error-highlight=}" - return - ;; - --*) - __gitcomp "$__git_diff_difftool_options" - return - ;; - esac - __git_complete_revlist_file + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "" + fi + + if [ ${#COMPREPLY[@]} -eq 0 ]; then + __git_complete_index_file "--others --directory" + fi } __git_mergetools_common="diffuse diffmerge ecmerge emerge kdiff3 meld opendiff diff --git a/contrib/subtree/git-subtree.sh b/contrib/subtree/git-subtree.sh index 791fd8260c4703..e9c7ca7cf513b1 100755 --- a/contrib/subtree/git-subtree.sh +++ b/contrib/subtree/git-subtree.sh @@ -278,6 +278,20 @@ main () { "cmd_$arg_command" "$@" } +# Usage: reject_if_v2_config REV +# +# Bails if we find .git-subtree/config. This file is used by the RIIR +# git-subtree, which can read data from this script, but which generates +# data that this script cannot cope with. So if we find that the user's +# project has already been processed with the new tool, we stop, to +# avoid generating broken output. +reject_if_v2_config () { + local config=.git-subtree/config + if git rev-parse --verify -q "$rev:$config"; then + die "fatal: tree contains $config: has been processed with new standalone (Rust) git-subtree; use that tool instead of this one. See https://codeberg.org/diziet/git-subtree https://crates.io/crates/git-subtree" + fi +} + # Usage: cache_setup cache_setup () { assert test $# = 0 @@ -846,6 +860,7 @@ process_split_commit () { # Or: cmd_add REPOSITORY REF cmd_add () { + reject_if_v2_config HEAD ensure_clean if test $# -eq 1 @@ -934,6 +949,8 @@ cmd_split () { die "fatal: you must provide exactly one revision, and optionally a repository. Got: '$*'" fi + reject_if_v2_config "$rev" + # Now validate prefix against the commit, not the working tree if ! git cat-file -e "$rev:$dir" 2>/dev/null then @@ -1034,6 +1051,7 @@ cmd_merge () { then repository="$2" fi + reject_if_v2_config HEAD ensure_clean if test -n "$arg_addmerge_squash" diff --git a/contrib/subtree/t/t7900-subtree.sh b/contrib/subtree/t/t7900-subtree.sh index c10f283b38d92d..7c06da476b0485 100755 --- a/contrib/subtree/t/t7900-subtree.sh +++ b/contrib/subtree/t/t7900-subtree.sh @@ -439,6 +439,24 @@ test_expect_success 'split sub dir/ with --rejoin' ' ) ' +test_expect_success 'split fail on RIIR git subtree data' ' + subtree_test_create_repo "$test_count" && + subtree_test_create_repo "$test_count/sub proj" && + test_create_commit "$test_count" main1 && + test_create_commit "$test_count/sub proj" sub1 && + ( + cd "$test_count" && + git fetch ./"sub proj" HEAD && + git subtree add --prefix="sub dir" FETCH_HEAD && + # simulate RIIR git-subtree generated data + mkdir .git-subtree && + echo "# sabotage" >.git-subtree/config && + git add .git-subtree/config && + git commit -m sabotage && + test_must_fail git subtree split -P "sub dir" HEAD + ) +' + # Tests that commits from other subtrees are not processed as # part of a split. # diff --git a/delta.h b/delta.h index eb5c6d2fdb9c51..42a211905dd2b7 100644 --- a/delta.h +++ b/delta.h @@ -14,7 +14,7 @@ struct delta_index; * using free_delta_index(). */ struct delta_index * -create_delta_index(const void *buf, unsigned long bufsize); +create_delta_index(const void *buf, size_t bufsize); /* * free_delta_index: free the index created by create_delta_index() @@ -28,7 +28,7 @@ void free_delta_index(struct delta_index *index); * * Given pointer must be what create_delta_index() returned, or NULL. */ -unsigned long sizeof_delta_index(struct delta_index *index); +size_t sizeof_delta_index(struct delta_index *index); /* * create_delta: create a delta from given index for the given buffer @@ -42,8 +42,8 @@ unsigned long sizeof_delta_index(struct delta_index *index); */ void * create_delta(const struct delta_index *index, - const void *buf, unsigned long bufsize, - unsigned long *delta_size, unsigned long max_delta_size); + const void *buf, size_t bufsize, + size_t *delta_size, size_t max_delta_size); /* * diff_delta: create a delta from source buffer to target buffer @@ -54,9 +54,9 @@ create_delta(const struct delta_index *index, * updated with its size. The returned buffer must be freed by the caller. */ static inline void * -diff_delta(const void *src_buf, unsigned long src_bufsize, - const void *trg_buf, unsigned long trg_bufsize, - unsigned long *delta_size, unsigned long max_delta_size) +diff_delta(const void *src_buf, size_t src_bufsize, + const void *trg_buf, size_t trg_bufsize, + size_t *delta_size, size_t max_delta_size) { struct delta_index *index = create_delta_index(src_buf, src_bufsize); if (index) { diff --git a/diff-delta.c b/diff-delta.c index 43c339f01061ca..7cbedeb5071bcc 100644 --- a/diff-delta.c +++ b/diff-delta.c @@ -125,14 +125,14 @@ struct unpacked_index_entry { }; struct delta_index { - unsigned long memsize; + size_t memsize; const void *src_buf; - unsigned long src_size; + size_t src_size; unsigned int hash_mask; struct index_entry *hash[FLEX_ARRAY]; }; -struct delta_index * create_delta_index(const void *buf, unsigned long bufsize) +struct delta_index * create_delta_index(const void *buf, size_t bufsize) { unsigned int i, hsize, hmask, entries, prev_val, *hash_count; const unsigned char *data, *buffer = buf; @@ -140,7 +140,7 @@ struct delta_index * create_delta_index(const void *buf, unsigned long bufsize) struct unpacked_index_entry *entry, **hash; struct index_entry *packed_entry, **packed_hash; void *mem; - unsigned long memsize; + size_t memsize; if (!buf || !bufsize) return NULL; @@ -302,7 +302,7 @@ void free_delta_index(struct delta_index *index) free(index); } -unsigned long sizeof_delta_index(struct delta_index *index) +size_t sizeof_delta_index(struct delta_index *index) { if (index) return index->memsize; @@ -318,8 +318,8 @@ unsigned long sizeof_delta_index(struct delta_index *index) void * create_delta(const struct delta_index *index, - const void *trg_buf, unsigned long trg_size, - unsigned long *delta_size, unsigned long max_size) + const void *trg_buf, size_t trg_size, + size_t *delta_size, size_t max_size) { unsigned int i, val; off_t outpos, moff; diff --git a/diff.c b/diff.c index cfe515af4e1759..414532d09f5ba3 100644 --- a/diff.c +++ b/diff.c @@ -3586,7 +3586,7 @@ static unsigned char *deflate_it(char *data, unsigned long size, unsigned long *result_size) { - int bound; + size_t bound; unsigned char *deflated; git_zstream stream; struct repo_config_values *cfg = repo_config_values(the_repository); @@ -3624,9 +3624,11 @@ static void emit_binary_diff_body(struct diff_options *o, delta = NULL; deflated = deflate_it(two->ptr, two->size, &deflate_size); if (one->size && two->size) { + size_t delta_size_st = 0; delta = diff_delta(one->ptr, one->size, two->ptr, two->size, - &delta_size, deflate_size); + &delta_size_st, deflate_size); + delta_size = cast_size_t_to_ulong(delta_size_st); if (delta) { void *to_free = delta; orig_size = delta_size; @@ -6163,7 +6165,7 @@ struct option *add_diff_options(const struct option *opts, N_("continue listing the history of a file beyond renames"), PARSE_OPT_NOARG, diff_opt_follow), OPT_INTEGER('l', NULL, &options->rename_limit, - N_("prevent rename/copy detection if the number of rename/copy targets exceeds given limit")), + N_("limit to cheap rename/copy detection if the number of rename/copy targets exceeds this value")), OPT_GROUP(N_("Diff algorithm options")), OPT_CALLBACK_F(0, "minimal", options, NULL, diff --git a/fetch-object-info.c b/fetch-object-info.c index ba7e179c44ee54..0a58308f9b2559 100644 --- a/fetch-object-info.c +++ b/fetch-object-info.c @@ -1,6 +1,7 @@ #include "git-compat-util.h" #include "gettext.h" #include "hex.h" +#include "object.h" #include "pkt-line.h" #include "connect.h" #include "oid-array.h" @@ -9,20 +10,26 @@ #include "string-list.h" /* Sends object-info command and its arguments into the request buffer. */ -static void send_object_info_request(const int fd_out, struct object_info_args *args) +static void send_object_info_request(const int fd_out, + const struct string_list *server_options, + const struct oid_array *oids, + unsigned ask_size, + unsigned ask_type) { struct strbuf req_buf = STRBUF_INIT; - write_command_and_capabilities(&req_buf, "object-info", args->server_options); + write_command_and_capabilities(&req_buf, "object-info", server_options); - if (unsorted_string_list_has_string(args->object_info_options, "size")) + if (ask_size) packet_buf_write(&req_buf, "size"); - else if (args->object_info_options->nr) - BUG("only size should be in object_info_options"); - if (args->oids) - for (size_t i = 0; i < args->oids->nr; i++) - packet_buf_write(&req_buf, "oid %s", oid_to_hex(&args->oids->oid[i])); + if (ask_type) + packet_buf_write(&req_buf, "type"); + + if (oids) + for (size_t i = 0; i < oids->nr; i++) + packet_buf_write(&req_buf, "oid %s", + oid_to_hex(&oids->oid[i])); packet_buf_flush(&req_buf); if (write_in_full(fd_out, req_buf.buf, req_buf.len) < 0) @@ -45,35 +52,42 @@ static int parse_object_size(const char *s, size_t *res) return 0; } -int fetch_object_info(const enum protocol_version version, struct object_info_args *args, - struct packet_reader *reader, struct object_info *object_info_data, - const int stateless_rpc, const int fd_out) +void fetch_object_info(const enum protocol_version version, + const struct string_list *server_options, + const struct oid_array *oids, + struct packet_reader *reader, + struct fetch_object_info_results *results, + const int stateless_rpc, + const int fd_out) { + unsigned ask_size = 0; + unsigned ask_type = 0; int size_index = -1; + int type_index = -1; + size_t wanted; + + results->nr = oids->nr; + CALLOC_ARRAY(results->unrecognized, results->nr); switch (version) { case protocol_v2: if (!server_supports_v2("object-info")) die(_("object-info capability is not enabled on the server")); - /* - * When removing an element from the list it gets swapped by the - * last element, iterate backwards to prevent elements skipping - * evaluation. - * - * object_info_options->nr can be safely casted without overflow - * because the number of options is a small known number (the - * supported placeholders which currently are size and type). - */ - for (int i = (int)args->object_info_options->nr - 1; i >= 0; i--) - if (!server_supports_feature("object-info", - args->object_info_options->items[i].string, 0)) - unsorted_string_list_delete_item(args->object_info_options, i, 0); + + if (results->wants_size && + server_supports_feature("object-info", "size", 0)) + ask_size = 1; + + if (results->wants_type && + server_supports_feature("object-info", "type", 0)) + ask_type = 1; /* * Even if no options are left, we still send the oid so we get * at least an existence check. */ - send_object_info_request(fd_out, args); + send_object_info_request(fd_out, server_options, oids, ask_size, + ask_type); break; case protocol_v1: case protocol_v0: @@ -81,43 +95,49 @@ int fetch_object_info(const enum protocol_version version, struct object_info_ar case protocol_unknown_version: BUG("unknown protocol version"); } + wanted = ask_size + ask_type; - for (size_t i = 0; i < args->object_info_options->nr; i++) { + for (size_t i = 0; i < wanted; i++) { if (packet_reader_read(reader) != PACKET_READ_NORMAL) { check_stateless_delimiter(stateless_rpc, reader, "stateless delimiter expected"); - return -1; + die(_("object-info: expected %" PRIuMAX " attributes, got %" PRIuMAX), + (uintmax_t)wanted, (uintmax_t)i); } - if (!unsorted_string_list_has_string(args->object_info_options, reader->line)) - return -1; - if (!strcmp(reader->line, "size")) { - /* - * i is the number of supported options which currently - * is only size. No risk of overflow. - */ + if (!ask_size) + die(_("object-info: unrequested 'size' attribute")); + if (results->sizes) + die(_("object-info: duplicate 'size' attribute")); size_index = (int)i; - for (size_t j = 0; j < args->oids->nr; j++) - object_info_data[j].sizep = - xcalloc(1, sizeof(*object_info_data[j].sizep)); + CALLOC_ARRAY(results->sizes, results->nr); + } else if (!strcmp(reader->line, "type")) { + if (!ask_type) + die(_("object-info: unrequested 'type' attribute")); + if (results->types) + die(_("object-info: duplicate 'type' attribute")); + type_index = (int)i; + CALLOC_ARRAY(results->types, results->nr); } else { - BUG("only size is supported"); + die(_("object-info: unknown attribute '%s'"), + reader->line); } } - for (size_t i = 0; - packet_reader_read(reader) == PACKET_READ_NORMAL && - i < args->oids->nr; - i++) { + for (size_t i = 0; i < oids->nr; i++) { struct string_list object_info_values = STRING_LIST_INIT_DUP; + if (packet_reader_read(reader) != PACKET_READ_NORMAL) + die(_("object-info: expected %" PRIuMAX " objects, got %" PRIuMAX), + (uintmax_t)oids->nr, (uintmax_t)i); + string_list_split(&object_info_values, reader->line, " ", -1); if (strcmp(object_info_values.items[0].string, - oid_to_hex(&args->oids->oid[i]))) + oid_to_hex(&oids->oid[i]))) die(_("object-info: expected OID: %s, got %s"), - oid_to_hex(&args->oids->oid[i]), + oid_to_hex(&oids->oid[i]), object_info_values.items[0].string); /* @@ -127,30 +147,53 @@ int fetch_object_info(const enum protocol_version version, struct object_info_ar */ if (object_info_values.nr >= 2 && !strcmp(object_info_values.items[1].string, "")) { - object_info_data[i].unrecognized = 1; + results->unrecognized[i] = 1; string_list_clear(&object_info_values, 0); continue; } /* - * Because we filter the options to be only the supported by - * the server we expect the server to answer with the same - * number of attributes requested. + * Because we only ask for attributes the server said it + * supports, we expect the answer to have one value per + * requested attribute, plus the OID. */ - if (args->object_info_options->nr + 1 != object_info_values.nr) + if (wanted + 1 != object_info_values.nr) die("object-info: unexpected number of attributes: %s", reader->line); - if (size_index >= 0 && + if (results->sizes && parse_object_size(object_info_values.items[size_index + 1].string, - object_info_data[i].sizep)) - die("object-info: ref %s has invalid size %s", + &results->sizes[i])) + die("object-info: object %s has invalid size %s", object_info_values.items[0].string, object_info_values.items[size_index + 1].string); + if (results->types) { + const char *type_str = + object_info_values.items[type_index + 1].string; + int type = type_from_string_gently(type_str, -1, 1); + + if (type < 0) + die(_("object-info: object %s has invalid type '%s'"), + object_info_values.items[0].string, type_str); + + results->types[i] = type; + } + string_list_clear(&object_info_values, 0); } + + if (packet_reader_read(reader) != PACKET_READ_FLUSH) + die(_("object-info: expected flush after %" PRIuMAX " objects"), + (uintmax_t)oids->nr); + check_stateless_delimiter(stateless_rpc, reader, "stateless delimiter expected"); +} - return 0; +void free_fetch_object_info_results(struct fetch_object_info_results *results) +{ + free(results->sizes); + free(results->types); + free(results->unrecognized); + memset(results, 0, sizeof(*results)); } diff --git a/fetch-object-info.h b/fetch-object-info.h index 269cebb3f7df48..2fba96c6f7de52 100644 --- a/fetch-object-info.h +++ b/fetch-object-info.h @@ -1,25 +1,40 @@ #ifndef FETCH_OBJECT_INFO_H #define FETCH_OBJECT_INFO_H +#include "object.h" #include "pkt-line.h" #include "protocol.h" -struct object_info_args { - struct string_list *object_info_options; - const struct string_list *server_options; - struct oid_array *oids; +struct fetch_object_info_results { + size_t *sizes; + enum object_type *types; + uint8_t *unrecognized; + size_t nr; + unsigned wants_size:1; + unsigned wants_type:1; }; -struct object_info; +#define FETCH_OBJECT_INFO_RESULTS_INIT { 0 } + +struct oid_array; /* - * Sends git-cat-file object-info command into the request buf and read the + * Sends git-cat-file object-info command into the request buf and reads the * results from packets. * - * Modifies args->object_info_options, on return it contains only the supported - * options by the server. + * The caller sets the wants_* flags in "results" to indicate which attributes + * it is interested in. On return, "results" holds one array per attribute that + * the server both advertised and answered with. An array left NULL means the + * attribute is not available. + * Release them with free_fetch_object_info_results(). */ -int fetch_object_info(enum protocol_version version, struct object_info_args *args, - struct packet_reader *reader, struct object_info *object_info_data, - int stateless_rpc, int fd_out); +void fetch_object_info(enum protocol_version version, + const struct string_list *server_options, + const struct oid_array *oids, + struct packet_reader *reader, + struct fetch_object_info_results *results, + int stateless_rpc, + int fd_out); + +void free_fetch_object_info_results(struct fetch_object_info_results *results); #endif /* FETCH_OBJECT_INFO_H */ diff --git a/git-compat-util.h b/git-compat-util.h index a0f901ce793ed6..a2d8651ed748cd 100644 --- a/git-compat-util.h +++ b/git-compat-util.h @@ -162,6 +162,9 @@ static inline int is_xplatform_dir_sep(int c) #include "compat/win32/path-utils.h" #include "compat/msvc.h" #endif +#ifdef DARWIN_REGEXEC +#include "compat/darwin.h" +#endif /* used on Mac OS X */ #ifdef PRECOMPOSE_UNICODE @@ -996,6 +999,7 @@ static inline int strtol_i(char const *s, int base, int *result) #error "Git requires REG_STARTEND support. Compile with NO_REGEX=NeedsStartEnd" #endif +#ifndef regexec_buf static inline int regexec_buf(const regex_t *preg, const char *buf, size_t size, size_t nmatch, regmatch_t pmatch[], int eflags) { @@ -1004,6 +1008,7 @@ static inline int regexec_buf(const regex_t *preg, const char *buf, size_t size, pmatch[0].rm_eo = size; return regexec(preg, buf, nmatch, pmatch, eflags | REG_STARTEND); } +#endif #ifdef USE_ENHANCED_BASIC_REGULAR_EXPRESSIONS int git_regcomp(regex_t *preg, const char *pattern, int cflags); diff --git a/git-send-email.perl b/git-send-email.perl index bb8ddd1eef2c25..2071cff6ae3f0a 100755 --- a/git-send-email.perl +++ b/git-send-email.perl @@ -863,7 +863,7 @@ sub get_patch_subject { return "GIT: $1\n"; } close $fh; - die sprintf(__("No subject line in %s?"), $fn); + die sprintf(__("No 'Subject:' line in '%s'\n"), $fn); } if ($compose) { diff --git a/git-zlib.c b/git-zlib.c index d21adb3bf5b15e..1c94f90497ee47 100644 --- a/git-zlib.c +++ b/git-zlib.c @@ -33,7 +33,7 @@ static const char *zerr_to_string(int status) /* uLong is 32-bit on Windows, even on 64-bit systems */ #define ULONG_MAX_VALUE maximum_unsigned_value_of_type(uLong) -static inline uInt zlib_buf_cap(unsigned long len) +static inline uInt zlib_buf_cap(size_t len) { return (ZLIB_BUF_MAX < len) ? ZLIB_BUF_MAX : len; } @@ -167,9 +167,21 @@ int git_inflate(git_zstream *strm, int flush) return status; } -unsigned long git_deflate_bound(git_zstream *strm, unsigned long size) +size_t git_deflate_bound(git_zstream *strm, size_t size) { - return deflateBound(&strm->z, size); +#if SIZE_MAX > ULONG_MAX + if (size > maximum_unsigned_value_of_type(uLong)) + /* + * deflateBound() takes uLong, which is 32-bit on + * Windows. For inputs above that range, return zlib's + * stored-block formula (the conservative path it would + * itself use for an unknown stream state) plus the + * worst-case wrapper overhead. + */ + return size + (size >> 5) + (size >> 7) + (size >> 11) + + 7 + 18; +#endif + return deflateBound(&strm->z, (uLong)size); } void git_deflate_init(git_zstream *strm, int level) diff --git a/git-zlib.h b/git-zlib.h index 44380e8ad38305..9248d11ca9622c 100644 --- a/git-zlib.h +++ b/git-zlib.h @@ -5,8 +5,8 @@ typedef struct git_zstream { struct z_stream_s z; - unsigned long avail_in; - unsigned long avail_out; + size_t avail_in; + size_t avail_out; size_t total_in; size_t total_out; unsigned char *next_in; @@ -25,6 +25,6 @@ void git_deflate_end(git_zstream *); int git_deflate_abort(git_zstream *); int git_deflate_end_gently(git_zstream *); int git_deflate(git_zstream *, int flush); -unsigned long git_deflate_bound(git_zstream *, unsigned long); +size_t git_deflate_bound(git_zstream *, size_t); #endif /* GIT_ZLIB_H */ diff --git a/git.c b/git.c index e5f1811b6bb762..d7b5f88e52ae87 100644 --- a/git.c +++ b/git.c @@ -202,7 +202,7 @@ static int handle_options(const char ***argv, int *argc, int *envchanged) if (envchanged) *envchanged = 1; } else if (!strcmp(cmd, "--no-lazy-fetch")) { - fetch_if_missing = 0; + the_repository->fetch_if_missing = 0; setenv(NO_LAZY_FETCH_ENVIRONMENT, "1", 1); if (envchanged) *envchanged = 1; @@ -304,11 +304,15 @@ static int handle_options(const char ***argv, int *argc, int *envchanged) if (envchanged) *envchanged = 1; } else if (!strcmp(cmd, "--shallow-file")) { - (*argv)++; - (*argc)--; - setenv(GIT_SHALLOW_FILE_ENVIRONMENT, (*argv)[0], 1); + if (*argc < 2) { + fprintf(stderr, _("no file given for '%s' option\n" ), "--shallow-file"); + usage(git_usage_string); + } + setenv(GIT_SHALLOW_FILE_ENVIRONMENT, (*argv)[1], 1); if (envchanged) *envchanged = 1; + (*argv)++; + (*argc)--; } else if (!strcmp(cmd, "-C")) { if (*argc < 2) { fprintf(stderr, _("no directory given for '%s' option\n" ), "-C"); diff --git a/http-push.c b/http-push.c index 786a2e9c0d0546..b8f3faaed95a31 100644 --- a/http-push.c +++ b/http-push.c @@ -367,7 +367,7 @@ static void start_put(struct transfer_request *request) void *unpacked; size_t len; int hdrlen; - ssize_t size; + size_t size; git_zstream stream; struct repo_config_values *cfg = repo_config_values(the_repository); diff --git a/http.c b/http.c index a0d399b2745ae6..c8fcfd7693e897 100644 --- a/http.c +++ b/http.c @@ -1608,6 +1608,8 @@ struct active_request_slot *get_active_slot(void) if (!slot->curl) { slot->curl = curl_easy_duphandle(curl_default); + if (!slot->curl) + die("curl_easy_duphandle failed"); curl_session_count++; } diff --git a/list-objects-filter.c b/list-objects-filter.c index c912ff3079a7d7..10e44f135780ba 100644 --- a/list-objects-filter.c +++ b/list-objects-filter.c @@ -828,3 +828,48 @@ void list_objects_filter__free(struct filter *filter) filter->free_fn(filter->filter_data); free(filter); } + +/* + * NEEDSWORK: this reimplements the blob:limit size check rather than + * reusing the existing filter machinery in + * list_objects_filter__filter_object(). That machinery is currently + * tied to the object-walk path and cannot easily be driven from a + * plain oidset. It would be nice to refactor the filter code so this + * helper can reuse it instead of duplicating the size check. + */ +int list_objects_filter__filter_oidset(struct repository *r, + const struct list_objects_filter_options *opts, + const struct oidset *in, + struct oidset *omitted) +{ + struct oidset_iter iter; + const struct object_id *oid; + + if (opts->choice != LOFC_BLOB_LIMIT) + return error(_("filter_oidset: only blob:limit filters are supported")); + + oidset_iter_init(in, &iter); + while ((oid = oidset_iter_next(&iter))) { + struct object_info info = OBJECT_INFO_INIT; + enum object_type type; + size_t size; + + info.typep = &type; + info.sizep = &size; + + /* + * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering + * a lazy fetch while inspecting candidates for removal. + */ + if (odb_read_object_info_extended(r->objects, oid, &info, + OBJECT_INFO_SKIP_FETCH_OBJECT) < 0) + continue; + + if (type != OBJ_BLOB) + continue; + + if (size >= opts->blob_limit_value) + oidset_insert(omitted, oid); + } + return 0; +} diff --git a/list-objects-filter.h b/list-objects-filter.h index 9e98814111cee4..5207ab70a9188e 100644 --- a/list-objects-filter.h +++ b/list-objects-filter.h @@ -94,4 +94,20 @@ enum list_objects_filter_result list_objects_filter__filter_object( */ void list_objects_filter__free(struct filter *filter); +/* + * Given a set of OIDs in 'in', populate 'omitted' with those that + * would be filtered by 'opts'. Currently only blob:limit=N is + * supported. Objects that cannot be read are silently skipped. + * + * NEEDSWORK: this reimplements the blob:limit size check rather than + * reusing the existing filter machinery. See the matching comment in + * list-objects-filter.c. + * + * Return 0 on success, -1 if the filter is not supported. + */ +int list_objects_filter__filter_oidset(struct repository *r, + const struct list_objects_filter_options *opts, + const struct oidset *in, + struct oidset *omitted); + #endif /* LIST_OBJECTS_FILTER_H */ diff --git a/loose.c b/loose.c index e97c3bb50af867..aa3cb1b4fce43b 100644 --- a/loose.c +++ b/loose.c @@ -61,12 +61,17 @@ static int insert_loose_map(struct odb_source_loose *loose, return inserted; } -static int load_one_loose_object_map(struct repository *repo, struct odb_source_loose *loose) +int loose_object_map_load(struct odb_source_loose *loose) { - struct strbuf buf = STRBUF_INIT, path = STRBUF_INIT; + struct repository *repo = loose->base.odb->repo; + struct strbuf buf = STRBUF_INIT; + char *path; FILE *fp; int ret = -1; + if (!should_use_loose_object_map(repo)) + return 0; + if (!loose->map) loose_object_map_init(&loose->map); if (!loose->cache) { @@ -78,10 +83,10 @@ static int load_one_loose_object_map(struct repository *repo, struct odb_source_ insert_loose_map(loose, repo->hash_algo->empty_blob, repo->compat_hash_algo->empty_blob); insert_loose_map(loose, repo->hash_algo->null_oid, repo->compat_hash_algo->null_oid); - repo_common_path_replace(repo, &path, "objects/loose-object-idx"); - fp = fopen(path.buf, "rb"); + path = xstrfmt("%s/loose-object-idx", loose->base.path); + fp = fopen(path, "rb"); if (!fp) { - strbuf_release(&path); + free(path); return 0; } @@ -102,7 +107,7 @@ static int load_one_loose_object_map(struct repository *repo, struct odb_source_ err: fclose(fp); strbuf_release(&buf); - strbuf_release(&path); + free(path); return ret; } @@ -110,17 +115,13 @@ int repo_read_loose_object_map(struct repository *repo) { struct odb_source *source; - if (!should_use_loose_object_map(repo)) - return 0; - odb_prepare_alternates(repo->objects); - for (source = repo->objects->sources; source; source = source->next) { struct odb_source_files *files = odb_source_files_downcast(source); - if (load_one_loose_object_map(repo, files->loose) < 0) { + if (loose_object_map_load(files->loose) < 0) return -1; - } } + return 0; } diff --git a/loose.h b/loose.h index 6c9b3f4571602f..ed663ac550fbb7 100644 --- a/loose.h +++ b/loose.h @@ -13,6 +13,7 @@ struct loose_object_map { void loose_object_map_init(struct loose_object_map **map); void loose_object_map_clear(struct loose_object_map **map); +int loose_object_map_load(struct odb_source_loose *loose); int repo_loose_object_map_oid(struct repository *repo, const struct object_id *src, const struct git_hash_algo *dest_algo, diff --git a/meson.build b/meson.build index 7073d5844d2531..6a965a0d72e5b4 100644 --- a/meson.build +++ b/meson.build @@ -1318,7 +1318,6 @@ elif host_machine.system() == 'windows' libgit_c_args += [ '-DDETECT_MSYS_TTY', - '-DENSURE_MSYSTEM_IS_SET', '-DNATIVE_CRLF', '-DNOGDI', '-DNO_POSIX_GOODIES', @@ -1328,6 +1327,20 @@ elif host_machine.system() == 'windows' '-D__USE_MINGW_ANSI_STDIO=0', ] + msystem = get_option('msystem') + if msystem != '' + mingw_prefix = get_option('mingw_prefix') + if mingw_prefix == '' + mingw_prefix = msystem.to_lower() + elif mingw_prefix.startswith('/') + mingw_prefix = mingw_prefix.substring(1) + endif + libgit_c_args += [ + '-DENSURE_MSYSTEM_IS_SET="' + msystem + '"', + '-DMINGW_PREFIX="' + mingw_prefix + '"' + ] + endif + libgit_dependencies += compiler.find_library('ntdll') libgit_include_directories += 'compat/win32' if compiler.get_id() == 'msvc' @@ -1407,6 +1420,11 @@ if not get_option('b_sanitize').contains('address') and get_option('regex').allo libgit_c_args += '-DUSE_ENHANCED_BASIC_REGULAR_EXPRESSIONS' compat_sources += 'compat/regcomp_enhanced.c' endif + + if host_machine.system() == 'darwin' + libgit_c_args += '-DDARWIN_REGEXEC' + compat_sources += 'compat/darwin/regexec.c' + endif elif not get_option('regex').enabled() libgit_c_args += [ '-DNO_REGEX', @@ -1464,6 +1482,7 @@ checkfuncs = { 'initgroups' : [], 'strtoumax' : ['strtoumax.c', 'strtoimax.c'], 'pread' : ['pread.c'], + 'writev' : ['writev.c'], } if host_machine.system() == 'windows' diff --git a/meson_options.txt b/meson_options.txt index dc88f130d754a1..becf4689bf63b1 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -21,6 +21,10 @@ option('runtime_prefix', type: 'boolean', value: false, description: 'Resolve ancillary tooling and support files relative to the location of the runtime binary instead of hard-coding them into the binary.') option('sane_tool_path', type: 'array', value: [], description: 'An array of paths to pick up tools from in case the normal tools are broken or lacking.') +option('msystem', type: 'string', value: '', + description: 'Fall-back on Windows when MSYSTEM is not set.') +option('mingw_prefix', type: 'string', value: '', + description: 'Fall-back on Windows when MINGW_PREFIX is not set.') # Build information compiled into Git and other parts like documentation. option('build_date', type: 'string', value: '', diff --git a/midx-write.c b/midx-write.c index 580724d21a677f..85371022543836 100644 --- a/midx-write.c +++ b/midx-write.c @@ -865,7 +865,7 @@ static void find_commits_for_midx_bitmap(struct commit_stack *commits, * complain later that we don't have reachability closure (and fail * appropriately). */ - fetch_if_missing = 0; + ctx->repo->fetch_if_missing = 0; revs.exclude_promisor_objects = 1; if (prepare_revision_walk(&revs)) diff --git a/object-file.c b/object-file.c index ec35c318bc9fe7..070d0796268acb 100644 --- a/object-file.c +++ b/object-file.c @@ -122,7 +122,7 @@ int check_object_signature(struct repository *r, const struct object_id *oid, } int stream_object_signature(struct repository *r, - struct odb_read_stream *st, + struct odb_stream *st, const struct object_id *oid) { struct object_id real_oid; @@ -138,7 +138,7 @@ int stream_object_signature(struct repository *r, git_hash_update(&c, hdr, hdrlen); for (;;) { char buf[1024 * 16]; - ssize_t readlen = odb_read_stream_read(st, buf, sizeof(buf)); + ssize_t readlen = odb_stream_read(st, buf, sizeof(buf)); if (readlen < 0) return -1; if (!readlen) @@ -702,9 +702,9 @@ static void prepare_packfile_transaction(struct odb_transaction_files *transacti die_errno("unable to write pack header"); } -static int hash_blob_stream(struct odb_write_stream *stream, - const struct git_hash_algo *hash_algo, - struct object_id *result_oid, size_t size) +static int hash_stream(struct odb_stream *stream, + const struct git_hash_algo *hash_algo, + struct object_id *result_oid) { unsigned char buf[16384]; struct git_hash_ctx ctx; @@ -712,22 +712,23 @@ static int hash_blob_stream(struct odb_write_stream *stream, size_t bytes_hashed = 0; header_len = format_object_header((char *)buf, sizeof(buf), - OBJ_BLOB, size); + stream->type, stream->size); git_hash_init(&ctx, hash_algo); git_hash_update(&ctx, buf, header_len); - while (!stream->is_finished) { - ssize_t read_result = odb_write_stream_read(stream, buf, - sizeof(buf)); - + while (1) { + ssize_t read_result = odb_stream_read(stream, buf, + sizeof(buf)); if (read_result < 0) return -1; + if (!read_result) + break; git_hash_update(&ctx, buf, read_result); bytes_hashed += read_result; } - if (bytes_hashed != size) + if (bytes_hashed != stream->size) return -1; git_hash_final_oid(result_oid, &ctx); @@ -739,9 +740,9 @@ static int hash_blob_stream(struct odb_write_stream *stream, * Read the contents from the stream provided, streaming it to the * packfile in state while updating the hash in ctx. */ -static void stream_blob_to_pack(struct transaction_packfile *state, - struct git_hash_ctx *ctx, size_t size, - struct odb_write_stream *stream) +static void stream_to_pack(struct transaction_packfile *state, + struct git_hash_ctx *ctx, + struct odb_stream *stream) { git_zstream s; unsigned char ibuf[16384]; @@ -749,21 +750,23 @@ static void stream_blob_to_pack(struct transaction_packfile *state, unsigned hdrlen; int status = Z_OK; struct repo_config_values *cfg = repo_config_values(the_repository); + bool is_finished = false; size_t bytes_read = 0; git_deflate_init(&s, cfg->pack_compression_level); - hdrlen = encode_in_pack_object_header(obuf, sizeof(obuf), OBJ_BLOB, size); + hdrlen = encode_in_pack_object_header(obuf, sizeof(obuf), stream->type, stream->size); s.next_out = obuf + hdrlen; s.avail_out = sizeof(obuf) - hdrlen; while (status != Z_STREAM_END) { - if (!stream->is_finished && !s.avail_in) { - ssize_t rsize = odb_write_stream_read(stream, ibuf, - sizeof(ibuf)); - + if (!is_finished && !s.avail_in) { + ssize_t rsize = odb_stream_read(stream, ibuf, + sizeof(ibuf)); if (rsize < 0) - die("failed to read blob data"); + die("failed to read object data"); + if (!rsize) + is_finished = true; git_hash_update(ctx, ibuf, rsize); @@ -772,7 +775,7 @@ static void stream_blob_to_pack(struct transaction_packfile *state, bytes_read += rsize; } - status = git_deflate(&s, stream->is_finished ? Z_FINISH : 0); + status = git_deflate(&s, is_finished ? Z_FINISH : 0); if (!s.avail_out || status == Z_STREAM_END) { size_t written = s.next_out - obuf; @@ -793,9 +796,9 @@ static void stream_blob_to_pack(struct transaction_packfile *state, } } - if (bytes_read != size) - die("read %" PRIuMAX " bytes of blob data, but expected %" PRIuMAX " bytes", - (uintmax_t)bytes_read, (uintmax_t)size); + if (bytes_read != stream->size) + die("read %" PRIuMAX " bytes of object data, but expected %" PRIuMAX " bytes", + (uintmax_t)bytes_read, (uintmax_t)stream->size); git_deflate_end(&s); } @@ -865,12 +868,11 @@ static void flush_packfile_transaction(struct odb_transaction_files *transaction * result, which we need to know beforehand when writing a git object. * Since the primary motivation for trying to stream from the working * tree file and to avoid mmaping it in core is to deal with large - * binary blobs, they generally do not want to get any conversion, and + * objects, they generally do not want to get any conversion, and * callers should avoid this code path when filters are requested. */ static int odb_transaction_files_write_object_stream(struct odb_transaction *base, - struct odb_write_stream *stream, - size_t size, + struct odb_stream *stream, struct object_id *result_oid) { struct odb_transaction_files *transaction = container_of(base, @@ -884,7 +886,7 @@ static int odb_transaction_files_write_object_stream(struct odb_transaction *bas struct pack_idx_entry *idx; header_len = format_object_header((char *)obuf, sizeof(obuf), - OBJ_BLOB, size); + stream->type, stream->size); git_hash_init(&ctx, transaction->base.source->odb->repo->hash_algo); git_hash_update(&ctx, obuf, header_len); @@ -899,7 +901,7 @@ static int odb_transaction_files_write_object_stream(struct odb_transaction *bas * to zlib compression and is sufficient for this check. */ if (state->nr_written && pack_size_limit_cfg && - pack_size_limit_cfg < state->offset + size) + pack_size_limit_cfg < state->offset + stream->size) flush_packfile_transaction(transaction); CALLOC_ARRAY(idx, 1); @@ -909,7 +911,7 @@ static int odb_transaction_files_write_object_stream(struct odb_transaction *bas hashfile_checkpoint(state->f, &checkpoint); idx->offset = state->offset; crc32_begin(state->f); - stream_blob_to_pack(state, &ctx, size, stream); + stream_to_pack(state, &ctx, stream); git_hash_final_oid(result_oid, &ctx); idx->crc32 = crc32_end(state->f); @@ -950,8 +952,8 @@ int index_fd(struct index_state *istate, struct object_id *oid, ret = index_core(istate, oid, fd, xsize_t(st->st_size), type, path, flags); } else { - struct odb_write_stream stream; - odb_write_stream_from_fd(&stream, fd, xsize_t(st->st_size)); + struct odb_stream *stream = odb_stream_from_fd(fd, xsize_t(st->st_size), + OBJ_BLOB); if (flags & INDEX_WRITE_OBJECT) { struct object_database *odb = the_repository->objects; @@ -961,18 +963,14 @@ int index_fd(struct index_state *istate, struct object_id *oid, if (!inflight) odb_transaction_begin_or_die(odb, &transaction, 0); ret = odb_transaction_write_object_stream(transaction, - &stream, - xsize_t(st->st_size), - oid); + stream, oid); if (!inflight) odb_transaction_commit(transaction); } else { - ret = hash_blob_stream(&stream, - the_repository->hash_algo, oid, - xsize_t(st->st_size)); + ret = hash_stream(stream, the_repository->hash_algo, oid); } - odb_write_stream_release(&stream); + odb_stream_close(stream); } close(fd); @@ -1340,13 +1338,3 @@ int odb_transaction_files_begin(struct odb_source *source, return 0; } - -void free_object_info_contents(struct object_info *object_info) -{ - if (!object_info) - return; - free(object_info->typep); - free(object_info->sizep); - free(object_info->disk_sizep); - free(object_info->delta_base_oid); -} diff --git a/object-file.h b/object-file.h index 805f2cfa289661..f44758c4f8ba01 100644 --- a/object-file.h +++ b/object-file.h @@ -101,7 +101,7 @@ int check_object_signature(struct repository *r, const struct object_id *oid, * the streaming interface and rehash it to do the same. */ int stream_object_signature(struct repository *r, - struct odb_read_stream *stream, + struct odb_stream *stream, const struct object_id *oid); enum finalize_object_file_flags { diff --git a/object.c b/object.c index 23b84aa7e29531..97f7fc0e87a1db 100644 --- a/object.c +++ b/object.c @@ -345,7 +345,7 @@ struct object *parse_object_with_flags(struct repository *r, if ((!obj || obj->type == OBJ_NONE || obj->type == OBJ_BLOB) && odb_read_object_info(r->objects, oid, NULL) == OBJ_BLOB) { if (!skip_hash) { - struct odb_read_stream *stream = odb_read_stream_open(r->objects, oid, NULL); + struct odb_stream *stream = odb_stream_from_object(r->objects, oid, NULL); if (!stream) { error(_("unable to open object stream for %s"), oid_to_hex(oid)); @@ -354,11 +354,11 @@ struct object *parse_object_with_flags(struct repository *r, if (stream_object_signature(r, stream, repl) < 0) { error(_("hash mismatch %s"), oid_to_hex(oid)); - odb_read_stream_close(stream); + odb_stream_close(stream); return NULL; } - odb_read_stream_close(stream); + odb_stream_close(stream); } parse_blob_buffer(lookup_blob(r, oid)); return lookup_object(r, oid); diff --git a/odb.c b/odb.c index caf1d0f5424d29..a456d29ce12d18 100644 --- a/odb.c +++ b/odb.c @@ -528,8 +528,6 @@ void disable_obj_read_lock(void) pthread_mutex_destroy(&obj_read_mutex); } -int fetch_if_missing = 1; - static int register_all_submodule_sources(struct object_database *odb) { int ret = odb->submodule_source_paths.nr; @@ -595,7 +593,7 @@ static int do_oid_object_info_extended(struct object_database *odb, continue; /* Check if it is a missing object */ - if (fetch_if_missing && repo_has_promisor_remote(odb->repo) && + if (odb->repo->fetch_if_missing && repo_has_promisor_remote(odb->repo) && !already_retried && !(flags & OBJECT_INFO_SKIP_FETCH_OBJECT)) { promisor_remote_get_direct(odb->repo, real, 1); @@ -1028,10 +1026,10 @@ int odb_write_object_ext(struct object_database *odb, } int odb_write_object_stream(struct object_database *odb, - struct odb_write_stream *stream, size_t len, + struct odb_stream *stream, struct object_id *oid) { - return odb_source_write_object_stream(odb->sources, stream, len, oid); + return odb_source_write_object_stream(odb->sources, stream, oid); } int odb_optimize(struct object_database *odb, @@ -1047,26 +1045,29 @@ bool odb_optimize_required(struct object_database *odb, } struct object_database *odb_new(struct repository *repo, - const char *primary_source, - const char *secondary_sources) + enum odb_new_flags flags) { - struct object_database *o = xmalloc(sizeof(*o)); - char *to_free = NULL; + char *primary_source = NULL, *secondary_sources = NULL; + struct object_database *o; - memset(o, 0, sizeof(*o)); + CALLOC_ARRAY(o, 1); o->repo = repo; pthread_mutex_init(&o->replace_mutex, NULL); string_list_init_dup(&o->submodule_source_paths); + if (flags & ODB_NEW_HONOR_ENV) { + primary_source = xstrdup_or_null(getenv(DB_ENVIRONMENT)); + secondary_sources = xstrdup_or_null(getenv(ALTERNATE_DB_ENVIRONMENT)); + } if (!primary_source) - primary_source = to_free = xstrfmt("%s/objects", repo->commondir); + primary_source = xstrfmt("%s/objects", repo->commondir); + o->sources = odb_source_new(o, primary_source, true); o->sources_tail = &o->sources->next; - o->alternate_db = xstrdup_or_null(secondary_sources); + o->alternate_db = secondary_sources; o->inmemory_objects = &odb_source_inmemory_new(o)->base; - free(to_free); - + free(primary_source); return o; } diff --git a/odb.h b/odb.h index fca67e8253e7ad..7cb44f5a2719d3 100644 --- a/odb.h +++ b/odb.h @@ -15,14 +15,6 @@ struct repository; struct strbuf; struct strvec; -/* - * Set this to 0 to prevent odb_read_object_info_extended() from fetching missing - * blobs. This has a difference only if extensions.partialClone is set. - * - * Its default value is 1. - */ -extern int fetch_if_missing; - /* * Compute the exact path an alternate is at and returns it. In case of * error NULL is returned and the human readable error is added to `err` @@ -100,6 +92,20 @@ struct object_database { struct string_list submodule_source_paths; }; +enum odb_new_flags { + /* + * Honor environment variables when constructing the object database + * sources. This makes us respect the following environment variables: + * + * - GIT_OBJECT_DIRECTORY to override the primary object directory. + * + * - GIT_ALTERNATE_OBJECT_DIRECTORIES to override alternates. + * + * Environment variables may be backend-specific. + */ + ODB_NEW_HONOR_ENV = (1 << 0), +}; + /* * Create a new object database for the given repository. * @@ -112,8 +118,7 @@ struct object_database { * Returns the newly created object database. */ struct object_database *odb_new(struct repository *repo, - const char *primary_source, - const char *alternate_sources); + enum odb_new_flags flags); /* Free the object database and release all resources. */ void odb_free(struct object_database *o); @@ -671,10 +676,10 @@ static inline int odb_write_object(struct object_database *odb, return odb_write_object_ext(odb, buf, len, type, oid, NULL, 0); } -struct odb_write_stream; +struct odb_stream; int odb_write_object_stream(struct object_database *odb, - struct odb_write_stream *stream, size_t len, + struct odb_stream *stream, struct object_id *oid); void parse_alternates(const char *string, @@ -682,7 +687,4 @@ void parse_alternates(const char *string, const char *relative_base, struct strvec *out); -/* Free pointers inside of object_info, but not object_info itself */ -void free_object_info_contents(struct object_info *object_info); - #endif /* ODB_H */ diff --git a/odb/source-files.c b/odb/source-files.c index 5a68af7d84c250..3945d651b92cd7 100644 --- a/odb/source-files.c +++ b/odb/source-files.c @@ -22,8 +22,7 @@ #include "tree.h" #include "write-or-die.h" -static void odb_source_files_reparent(const char *name UNUSED, - const char *old_cwd, +static void odb_source_files_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -37,7 +36,7 @@ static void odb_source_files_reparent(const char *name UNUSED, static void odb_source_files_free(struct odb_source *source) { struct odb_source_files *files = odb_source_files_downcast(source); - chdir_notify_unregister(NULL, odb_source_files_reparent, files); + chdir_notify_unregister(odb_source_files_reparent, files); odb_source_free(&files->loose->base); odb_source_free(&files->packed->base); odb_source_release(&files->base); @@ -51,6 +50,23 @@ static void odb_source_files_close(struct odb_source *source) odb_source_close(&files->packed->base); } +static int odb_source_files_create_on_disk(struct odb_source *source) +{ + struct strbuf path = STRBUF_INIT; + + safe_create_dir(source->odb->repo, source->path, 1); + + strbuf_addf(&path, "%s/pack", source->path); + safe_create_dir(source->odb->repo, path.buf, 1); + + strbuf_reset(&path); + strbuf_addf(&path, "%s/info", source->path); + safe_create_dir(source->odb->repo, path.buf, 1); + + strbuf_release(&path); + return 0; +} + static void odb_source_files_prepare(struct odb_source *source, enum odb_prepare_flags flags) { @@ -73,7 +89,7 @@ static int odb_source_files_read_object_info(struct odb_source *source, return -1; } -static int odb_source_files_read_object_stream(struct odb_read_stream **out, +static int odb_source_files_read_object_stream(struct odb_stream **out, struct odb_source *source, const struct object_id *oid) { @@ -184,12 +200,11 @@ static int odb_source_files_write_object(struct odb_source *source, } static int odb_source_files_write_object_stream(struct odb_source *source, - struct odb_write_stream *stream, - size_t len, + struct odb_stream *stream, struct object_id *oid) { struct odb_source_files *files = odb_source_files_downcast(source); - return odb_source_write_object_stream(&files->loose->base, stream, len, oid); + return odb_source_write_object_stream(&files->loose->base, stream, oid); } static int odb_source_files_begin_transaction(struct odb_source *source, @@ -742,6 +757,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, files->base.free = odb_source_files_free; files->base.close = odb_source_files_close; + files->base.create_on_disk = odb_source_files_create_on_disk; files->base.prepare = odb_source_files_prepare; files->base.read_object_info = odb_source_files_read_object_info; files->base.read_object_stream = odb_source_files_read_object_stream; @@ -763,7 +779,7 @@ struct odb_source_files *odb_source_files_new(struct object_database *odb, * paths in the primary ODB source in some user-facing functionality. */ if (!is_absolute_path(path)) - chdir_notify_register(NULL, odb_source_files_reparent, files); + chdir_notify_register(odb_source_files_reparent, files); return files; } diff --git a/odb/source-files.h b/odb/source-files.h index 044242bc36e4a7..9630b5f962e15f 100644 --- a/odb/source-files.h +++ b/odb/source-files.h @@ -43,7 +43,9 @@ bool odb_source_files_optimize_required(struct odb_source *source, static inline struct odb_source_files *odb_source_files_downcast(struct odb_source *source) { if (source->type != ODB_SOURCE_FILES) - BUG("trying to downcast source of type '%d' to files", source->type); + BUG("trying to downcast source of type '%s' to '%s'", + odb_source_type_to_name(source->type), + odb_source_type_to_name(ODB_SOURCE_FILES)); return container_of(source, struct odb_source_files, base); } diff --git a/odb/source-inmemory.c b/odb/source-inmemory.c index 3e71611b8e0071..795672adf255c6 100644 --- a/odb/source-inmemory.c +++ b/odb/source-inmemory.c @@ -73,12 +73,12 @@ static int odb_source_inmemory_read_object_info(struct odb_source *source, } struct odb_read_stream_inmemory { - struct odb_read_stream base; + struct odb_stream base; const unsigned char *buf; size_t offset; }; -static ssize_t odb_read_stream_inmemory_read(struct odb_read_stream *stream, +static ssize_t odb_read_stream_inmemory_read(struct odb_stream *stream, char *buf, size_t buf_len) { struct odb_read_stream_inmemory *inmemory = @@ -94,12 +94,12 @@ static ssize_t odb_read_stream_inmemory_read(struct odb_read_stream *stream, return bytes; } -static int odb_read_stream_inmemory_close(struct odb_read_stream *stream UNUSED) +static int odb_read_stream_inmemory_close(struct odb_stream *stream UNUSED) { return 0; } -static int odb_source_inmemory_read_object_stream(struct odb_read_stream **out, +static int odb_source_inmemory_read_object_stream(struct odb_stream **out, struct odb_source *source, const struct object_id *oid) { @@ -256,8 +256,7 @@ static int odb_source_inmemory_write_object(struct odb_source *source, } static int odb_source_inmemory_write_object_stream(struct odb_source *source, - struct odb_write_stream *stream, - size_t len, + struct odb_stream *stream, struct object_id *oid) { char buf[16384]; @@ -265,12 +264,19 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source, char *data; int ret; - CALLOC_ARRAY(data, len); - while (!stream->is_finished) { + CALLOC_ARRAY(data, stream->size); + while (1) { ssize_t bytes_read; - bytes_read = odb_write_stream_read(stream, buf, sizeof(buf)); - if (total_read + bytes_read > len) { + bytes_read = odb_stream_read(stream, buf, sizeof(buf)); + if (bytes_read < 0) { + ret = error("failed to read object stream"); + goto out; + } + if (!bytes_read) + break; + + if (total_read + bytes_read > stream->size) { ret = error("object stream yielded more bytes than expected"); goto out; } @@ -279,15 +285,16 @@ static int odb_source_inmemory_write_object_stream(struct odb_source *source, total_read += bytes_read; } - if (total_read != len) { + if (total_read != stream->size) { ret = error("object stream yielded less bytes than expected"); goto out; } - hash_object_file(source->odb->repo->hash_algo, data, total_read, OBJ_BLOB, oid); + hash_object_file(source->odb->repo->hash_algo, data, total_read, + stream->type, oid); - ret = odb_source_inmemory_write_object(source, data, len, OBJ_BLOB, oid, - NULL, NULL, 0); + ret = odb_source_inmemory_write_object(source, data, stream->size, + stream->type, oid, NULL, NULL, 0); if (ret < 0) goto out; diff --git a/odb/source-inmemory.h b/odb/source-inmemory.h index a88fc2e320ed5c..adbad23e8b26af 100644 --- a/odb/source-inmemory.h +++ b/odb/source-inmemory.h @@ -26,7 +26,9 @@ struct odb_source_inmemory *odb_source_inmemory_new(struct object_database *odb) static inline struct odb_source_inmemory *odb_source_inmemory_downcast(struct odb_source *source) { if (source->type != ODB_SOURCE_INMEMORY) - BUG("trying to downcast source of type '%d' to in-memory", source->type); + BUG("trying to downcast source of type '%s' to '%s'", + odb_source_type_to_name(source->type), + odb_source_type_to_name(ODB_SOURCE_INMEMORY)); return container_of(source, struct odb_source_inmemory, base); } diff --git a/odb/source-loose.c b/odb/source-loose.c index ef0e9192777c4a..0921a8748082ba 100644 --- a/odb/source-loose.c +++ b/odb/source-loose.c @@ -278,7 +278,7 @@ static void *odb_source_loose_map_object(struct odb_source_loose *loose, } struct odb_loose_read_stream { - struct odb_read_stream base; + struct odb_stream base; git_zstream z; enum { ODB_LOOSE_READ_STREAM_INUSE, @@ -292,7 +292,7 @@ struct odb_loose_read_stream { int hdr_used; }; -static ssize_t read_istream_loose(struct odb_read_stream *_st, char *buf, size_t sz) +static ssize_t read_istream_loose(struct odb_stream *_st, char *buf, size_t sz) { struct odb_loose_read_stream *st = container_of(_st, struct odb_loose_read_stream, base); @@ -339,7 +339,7 @@ static ssize_t read_istream_loose(struct odb_read_stream *_st, char *buf, size_t return total_read; } -static int close_istream_loose(struct odb_read_stream *_st) +static int close_istream_loose(struct odb_stream *_st) { struct odb_loose_read_stream *st = container_of(_st, struct odb_loose_read_stream, base); @@ -350,7 +350,7 @@ static int close_istream_loose(struct odb_read_stream *_st) return 0; } -static int odb_source_loose_read_object_stream(struct odb_read_stream **out, +static int odb_source_loose_read_object_stream(struct odb_stream **out, struct odb_source *source, const struct object_id *oid) { @@ -845,8 +845,7 @@ static int odb_source_loose_write_object(struct odb_source *source, } static int odb_source_loose_write_object_stream(struct odb_source *source, - struct odb_write_stream *in_stream, - size_t len, + struct odb_stream *in_stream, struct object_id *oid) { struct odb_source_loose *loose = odb_source_loose_downcast(source); @@ -860,6 +859,7 @@ static int odb_source_loose_write_object_stream(struct odb_source *source, struct strbuf filename = STRBUF_INIT; unsigned char buf[8192]; int dirlen; + bool is_finished = false; char hdr[MAX_HEADER_LEN]; int hdrlen; @@ -868,7 +868,7 @@ static int odb_source_loose_write_object_stream(struct odb_source *source, /* Since oid is not determined, save tmp file to odb path. */ strbuf_addf(&filename, "%s/", loose->base.path); - hdrlen = format_object_header(hdr, sizeof(hdr), OBJ_BLOB, len); + hdrlen = format_object_header(hdr, sizeof(hdr), in_stream->type, in_stream->size); /* * Common steps for write_loose_object and stream_loose_object to @@ -890,21 +890,24 @@ static int odb_source_loose_write_object_stream(struct odb_source *source, do { unsigned char *in0 = stream.next_in; - if (!stream.avail_in && !in_stream->is_finished) { - ssize_t read_len = odb_write_stream_read(in_stream, buf, - sizeof(buf)); + if (!stream.avail_in && !is_finished) { + ssize_t read_len = odb_stream_read(in_stream, buf, + sizeof(buf)); if (read_len < 0) { close(fd); err = -1; goto cleanup; } + /* All data has been read. */ + if (!read_len) { + is_finished = true; + flush = 1; + } + stream.avail_in = read_len; stream.next_in = buf; in0 = buf; - /* All data has been read. */ - if (in_stream->is_finished) - flush = 1; } ret = write_loose_object_common(loose, &c, &compat_c, &stream, flush, in0, fd, compressed, sizeof(compressed)); @@ -916,9 +919,9 @@ static int odb_source_loose_write_object_stream(struct odb_source *source, */ } while (ret == Z_OK || ret == Z_BUF_ERROR); - if (stream.total_in != len + hdrlen) + if (stream.total_in != in_stream->size + hdrlen) die(_("write stream object %"PRIuMAX" != %"PRIuMAX), (uintmax_t)stream.total_in, - (uintmax_t)len + hdrlen); + (uintmax_t)in_stream->size + hdrlen); /* * Common steps for write_loose_object and stream_loose_object to @@ -1006,8 +1009,7 @@ static void odb_source_loose_close(struct odb_source *source UNUSED) /* Nothing to do. */ } -static void odb_source_loose_reparent(const char *name UNUSED, - const char *old_cwd, +static void odb_source_loose_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -1023,7 +1025,7 @@ static void odb_source_loose_free(struct odb_source *source) struct odb_source_loose *loose = odb_source_loose_downcast(source); odb_source_loose_clear_cache(loose); loose_object_map_clear(&loose->map); - chdir_notify_unregister(NULL, odb_source_loose_reparent, loose); + chdir_notify_unregister(odb_source_loose_reparent, loose); odb_source_release(&loose->base); free(loose); } @@ -1053,7 +1055,9 @@ struct odb_source_loose *odb_source_loose_new(struct object_database *odb, loose->base.write_alternate = odb_source_loose_write_alternate; if (!is_absolute_path(loose->base.path)) - chdir_notify_register(NULL, odb_source_loose_reparent, loose); + chdir_notify_register(odb_source_loose_reparent, loose); + + loose_object_map_load(loose); return loose; } diff --git a/odb/source-loose.h b/odb/source-loose.h index 6070aaf3ce6ab2..3cf2e1f8f1d5e2 100644 --- a/odb/source-loose.h +++ b/odb/source-loose.h @@ -41,7 +41,9 @@ struct odb_source_loose *odb_source_loose_new(struct object_database *odb, static inline struct odb_source_loose *odb_source_loose_downcast(struct odb_source *source) { if (source->type != ODB_SOURCE_LOOSE) - BUG("trying to downcast source of type '%d' to loose", source->type); + BUG("trying to downcast source of type '%s' to '%s'", + odb_source_type_to_name(source->type), + odb_source_type_to_name(ODB_SOURCE_LOOSE)); return container_of(source, struct odb_source_loose, base); } diff --git a/odb/source-packed.c b/odb/source-packed.c index 0890704e76879b..9b23ea15dc1900 100644 --- a/odb/source-packed.c +++ b/odb/source-packed.c @@ -70,7 +70,7 @@ static int odb_source_packed_read_object_info(struct odb_source *source, return 0; } -static int odb_source_packed_read_object_stream(struct odb_read_stream **out, +static int odb_source_packed_read_object_stream(struct odb_stream **out, struct odb_source *source, const struct object_id *oid) { @@ -609,8 +609,7 @@ static int odb_source_packed_write_object(struct odb_source *source UNUSED, } static int odb_source_packed_write_object_stream(struct odb_source *source UNUSED, - struct odb_write_stream *stream UNUSED, - size_t len UNUSED, + struct odb_stream *stream UNUSED, struct object_id *oid UNUSED) { return error("packed backend cannot write object streams"); @@ -786,8 +785,7 @@ static void odb_source_packed_prepare(struct odb_source *source, packed->initialized = true; } -static void odb_source_packed_reparent(const char *name UNUSED, - const char *old_cwd, +static void odb_source_packed_reparent(const char *old_cwd, const char *new_cwd, void *cb_data) { @@ -816,7 +814,7 @@ static void odb_source_packed_free(struct odb_source *source) { struct odb_source_packed *packed = odb_source_packed_downcast(source); - chdir_notify_unregister(NULL, odb_source_packed_reparent, packed); + chdir_notify_unregister(odb_source_packed_reparent, packed); for (struct packfile_list_entry *e = packed->packs.head; e; e = e->next) free(e->pack); @@ -853,7 +851,7 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb, packed->base.write_alternate = odb_source_packed_write_alternate; if (!is_absolute_path(path)) - chdir_notify_register(NULL, odb_source_packed_reparent, packed); + chdir_notify_register(odb_source_packed_reparent, packed); return packed; } diff --git a/odb/source-packed.h b/odb/source-packed.h index 77309ddd0932b6..a0f6b5096dcd0f 100644 --- a/odb/source-packed.h +++ b/odb/source-packed.h @@ -78,7 +78,9 @@ struct odb_source_packed *odb_source_packed_new(struct object_database *odb, static inline struct odb_source_packed *odb_source_packed_downcast(struct odb_source *source) { if (source->type != ODB_SOURCE_PACKED) - BUG("trying to downcast source of type '%d' to packed", source->type); + BUG("trying to downcast source of type '%s' to '%s'", + odb_source_type_to_name(source->type), + odb_source_type_to_name(ODB_SOURCE_PACKED)); return container_of(source, struct odb_source_packed, base); } diff --git a/odb/source.c b/odb/source.c index 7993dcbd659399..30188b806d524d 100644 --- a/odb/source.c +++ b/odb/source.c @@ -4,6 +4,25 @@ #include "odb/source.h" #include "packfile.h" +static const char * const odb_source_names_by_type[] = { + [ODB_SOURCE_UNKNOWN] = "unknown", + [ODB_SOURCE_FILES] = "files", + [ODB_SOURCE_LOOSE] = "loose", + [ODB_SOURCE_PACKED] = "packed", + [ODB_SOURCE_INMEMORY] = "in-memory", +}; + +const char *odb_source_type_to_name(enum odb_source_type type) +{ + const char *name; + if (type < 0 || type >= ARRAY_SIZE(odb_source_names_by_type)) + type = ODB_SOURCE_UNKNOWN; + name = odb_source_names_by_type[type]; + if (!name) + BUG("name missing in `odb_source_names_by_type` for '%d'", type); + return name; +} + struct odb_source *odb_source_new(struct object_database *odb, const char *path, bool local) diff --git a/odb/source.h b/odb/source.h index d69f8e2d1cf626..fed5692096602b 100644 --- a/odb/source.h +++ b/odb/source.h @@ -25,8 +25,14 @@ enum odb_source_type { ODB_SOURCE_INMEMORY, }; +/* + * Convert between the enum and its name. Returns the equivalent of "unknown" + * for unknown types. + */ +const char *odb_source_type_to_name(enum odb_source_type type); + struct object_id; -struct odb_read_stream; +struct odb_stream; struct strvec; /* @@ -83,6 +89,18 @@ struct odb_source { */ void (*close)(struct odb_source *source); + /* + * This callback is expected to create on-disk data structures that are + * required for this source to operate. + * + * The callback is expected to return 0 on success, a negative error + * code otherwise. + * + * This callback may be NULL in case the source does not need any + * on-disk setup. + */ + int (*create_on_disk)(struct odb_source *source); + /* * This callback is expected to prepare the source so that it becomes * ready for use. It optionally clears underlying caches of the object @@ -125,7 +143,7 @@ struct odb_source { * The callback is expected to return a negative error code in case * creating the object stream has failed, 0 otherwise. */ - int (*read_object_stream)(struct odb_read_stream **out, + int (*read_object_stream)(struct odb_stream **out, struct odb_source *source, const struct object_id *oid); @@ -221,7 +239,7 @@ struct odb_source { * otherwise. */ int (*write_object_stream)(struct odb_source *source, - struct odb_write_stream *stream, size_t len, + struct odb_stream *stream, struct object_id *oid); /* @@ -327,6 +345,17 @@ static inline void odb_source_close(struct odb_source *source) source->close(source); } +/* + * Create on-disk data structures that are required for this source to operate + * correctly. Returns 0 on success, a negative error code otherwise. + */ +static inline int odb_source_create_on_disk(struct odb_source *source) +{ + if (!source->create_on_disk) + return 0; + return source->create_on_disk(source); +} + /* * Prepare the object database source and clear any caches. Depending on the * backend used this may have the effect that concurrently-written objects @@ -354,7 +383,7 @@ static inline int odb_source_read_object_info(struct odb_source *source, * Create a new read stream for the given object ID. Returns 0 on success, a * negative error code otherwise. */ -static inline int odb_source_read_object_stream(struct odb_read_stream **out, +static inline int odb_source_read_object_stream(struct odb_stream **out, struct odb_source *source, const struct object_id *oid) { @@ -451,11 +480,10 @@ static inline int odb_source_write_object(struct odb_source *source, * out pointer for the object ID. */ static inline int odb_source_write_object_stream(struct odb_source *source, - struct odb_write_stream *stream, - size_t len, + struct odb_stream *stream, struct object_id *oid) { - return source->write_object_stream(source, stream, len, oid); + return source->write_object_stream(source, stream, oid); } /* diff --git a/odb/streaming.c b/odb/streaming.c index 20531e864c9561..9c85ec54f59bb1 100644 --- a/odb/streaming.c +++ b/odb/streaming.c @@ -20,8 +20,8 @@ *****************************************************************/ struct odb_filtered_read_stream { - struct odb_read_stream base; - struct odb_read_stream *upstream; + struct odb_stream base; + struct odb_stream *upstream; struct stream_filter *filter; char ibuf[FILTER_BUFFER]; char obuf[FILTER_BUFFER]; @@ -30,14 +30,14 @@ struct odb_filtered_read_stream { int input_finished; }; -static int close_istream_filtered(struct odb_read_stream *_fs) +static int close_istream_filtered(struct odb_stream *_fs) { struct odb_filtered_read_stream *fs = (struct odb_filtered_read_stream *)_fs; free_stream_filter(fs->filter); - return odb_read_stream_close(fs->upstream); + return odb_stream_close(fs->upstream); } -static ssize_t read_istream_filtered(struct odb_read_stream *_fs, char *buf, +static ssize_t read_istream_filtered(struct odb_stream *_fs, char *buf, size_t sz) { struct odb_filtered_read_stream *fs = (struct odb_filtered_read_stream *)_fs; @@ -86,7 +86,7 @@ static ssize_t read_istream_filtered(struct odb_read_stream *_fs, char *buf, /* refill the input from the upstream */ if (!fs->input_finished) { - fs->i_end = odb_read_stream_read(fs->upstream, fs->ibuf, FILTER_BUFFER); + fs->i_end = odb_stream_read(fs->upstream, fs->ibuf, FILTER_BUFFER); if (fs->i_end < 0) return -1; if (fs->i_end) @@ -97,8 +97,8 @@ static ssize_t read_istream_filtered(struct odb_read_stream *_fs, char *buf, return filled; } -static struct odb_read_stream *attach_stream_filter(struct odb_read_stream *st, - struct stream_filter *filter) +static struct odb_stream *attach_stream_filter(struct odb_stream *st, + struct stream_filter *filter) { struct odb_filtered_read_stream *fs; @@ -120,19 +120,19 @@ static struct odb_read_stream *attach_stream_filter(struct odb_read_stream *st, *****************************************************************/ struct odb_incore_read_stream { - struct odb_read_stream base; + struct odb_stream base; char *buf; /* from odb_read_object_info_extended() */ unsigned long read_ptr; }; -static int close_istream_incore(struct odb_read_stream *_st) +static int close_istream_incore(struct odb_stream *_st) { struct odb_incore_read_stream *st = (struct odb_incore_read_stream *)_st; free(st->buf); return 0; } -static ssize_t read_istream_incore(struct odb_read_stream *_st, char *buf, size_t sz) +static ssize_t read_istream_incore(struct odb_stream *_st, char *buf, size_t sz) { struct odb_incore_read_stream *st = (struct odb_incore_read_stream *)_st; size_t read_size = sz; @@ -147,7 +147,7 @@ static ssize_t read_istream_incore(struct odb_read_stream *_st, char *buf, size_ return read_size; } -static int open_istream_incore(struct odb_read_stream **out, +static int open_istream_incore(struct odb_stream **out, struct object_database *odb, const struct object_id *oid) { @@ -178,7 +178,7 @@ static int open_istream_incore(struct odb_read_stream **out, * static helpers variables and functions for users of streaming interface *****************************************************************************/ -static int istream_source(struct odb_read_stream **out, +static int istream_source(struct odb_stream **out, struct object_database *odb, const struct object_id *oid) { @@ -196,23 +196,23 @@ static int istream_source(struct odb_read_stream **out, * Users of streaming interface ****************************************************************/ -int odb_read_stream_close(struct odb_read_stream *st) +int odb_stream_close(struct odb_stream *st) { int r = st->close(st); free(st); return r; } -ssize_t odb_read_stream_read(struct odb_read_stream *st, void *buf, size_t sz) +ssize_t odb_stream_read(struct odb_stream *st, void *buf, size_t sz) { return st->read(st, buf, sz); } -struct odb_read_stream *odb_read_stream_open(struct object_database *odb, - const struct object_id *oid, - struct stream_filter *filter) +struct odb_stream *odb_stream_from_object(struct object_database *odb, + const struct object_id *oid, + struct stream_filter *filter) { - struct odb_read_stream *st; + struct odb_stream *st; const struct object_id *real = lookup_replace_object(odb->repo, oid); int ret = istream_source(&st, odb, real); @@ -221,9 +221,9 @@ struct odb_read_stream *odb_read_stream_open(struct object_database *odb, if (filter) { /* Add "&& !is_null_stream_filter(filter)" for performance */ - struct odb_read_stream *nst = attach_stream_filter(st, filter); + struct odb_stream *nst = attach_stream_filter(st, filter); if (!nst) { - odb_read_stream_close(st); + odb_stream_close(st); return NULL; } st = nst; @@ -232,27 +232,17 @@ struct odb_read_stream *odb_read_stream_open(struct object_database *odb, return st; } -ssize_t odb_write_stream_read(struct odb_write_stream *st, void *buf, size_t sz) -{ - return st->read(st, buf, sz); -} - -void odb_write_stream_release(struct odb_write_stream *st) -{ - free(st->data); -} - int odb_stream_blob_to_fd(struct object_database *odb, int fd, const struct object_id *oid, struct stream_filter *filter, int can_seek) { - struct odb_read_stream *st; + struct odb_stream *st; ssize_t kept = 0; int result = -1; - st = odb_read_stream_open(odb, oid, filter); + st = odb_stream_from_object(odb, oid, filter); if (!st) { if (filter) free_stream_filter(filter); @@ -263,7 +253,7 @@ int odb_stream_blob_to_fd(struct object_database *odb, for (;;) { char buf[1024 * 16]; ssize_t wrote, holeto; - ssize_t readlen = odb_read_stream_read(st, buf, sizeof(buf)); + ssize_t readlen = odb_stream_read(st, buf, sizeof(buf)); if (readlen < 0) goto close_and_exit; @@ -294,47 +284,53 @@ int odb_stream_blob_to_fd(struct object_database *odb, result = 0; close_and_exit: - odb_read_stream_close(st); + odb_stream_close(st); return result; } -struct read_object_fd_data { +struct fd_stream { + struct odb_stream base; int fd; size_t remaining; }; -static ssize_t read_object_fd(struct odb_write_stream *stream, - unsigned char *buf, size_t len) +static ssize_t fd_stream_read(struct odb_stream *stream, + char *buf, size_t len) { - struct read_object_fd_data *data = stream->data; + struct fd_stream *fds = container_of(stream, struct fd_stream, base); ssize_t read_result; size_t count; - if (stream->is_finished) + if (!fds->remaining) return 0; - count = data->remaining < len ? data->remaining : len; - read_result = read_in_full(data->fd, buf, count); + count = fds->remaining < len ? fds->remaining : len; + read_result = read_in_full(fds->fd, buf, count); if (read_result < 0 || (size_t)read_result != count) return -1; - data->remaining -= count; - if (!data->remaining) - stream->is_finished = 1; + fds->remaining -= count; return read_result; } -void odb_write_stream_from_fd(struct odb_write_stream *stream, int fd, - size_t size) +static int fd_stream_close(struct odb_stream *stream UNUSED) +{ + /* The file descriptor is owned by the caller for now. */ + return 0; +} + +struct odb_stream *odb_stream_from_fd(int fd, size_t size, enum object_type type) { - struct read_object_fd_data *data; + struct fd_stream *fds; - CALLOC_ARRAY(data, 1); - data->fd = fd; - data->remaining = size; + CALLOC_ARRAY(fds, 1); + fds->base.read = fd_stream_read; + fds->base.close = fd_stream_close; + fds->base.size = size; + fds->base.type = type; + fds->fd = fd; + fds->remaining = size; - stream->data = data; - stream->read = read_object_fd; - stream->is_finished = 0; + return &fds->base; } diff --git a/odb/streaming.h b/odb/streaming.h index c0236717802301..b522ff513f26d2 100644 --- a/odb/streaming.h +++ b/odb/streaming.h @@ -8,68 +8,53 @@ #include "odb.h" struct object_database; -struct odb_read_stream; +struct odb_stream; struct stream_filter; -typedef int (*odb_read_stream_close_fn)(struct odb_read_stream *); -typedef ssize_t (*odb_read_stream_read_fn)(struct odb_read_stream *, char *, size_t); +typedef int (*odb_stream_close_fn)(struct odb_stream *); +typedef ssize_t (*odb_stream_read_fn)(struct odb_stream *, char *, size_t); /* - * A stream that can be used to read an object from the object database without - * loading all of it into memory. + * A stream that can be used to read an object from or write an object into the + * object database without loading all of it into memory. */ -struct odb_read_stream { - odb_read_stream_close_fn close; - odb_read_stream_read_fn read; +struct odb_stream { + odb_stream_close_fn close; + odb_stream_read_fn read; enum object_type type; size_t size; /* inflated size of full object */ }; /* - * Create a new object stream for the given object database. An optional filter - * can be used to transform the object's content. + * Create a new object stream for the given object. An optional filter can be + * used to transform the object's content. * * Returns the stream on success, a `NULL` pointer otherwise. */ -struct odb_read_stream *odb_read_stream_open(struct object_database *odb, - const struct object_id *oid, - struct stream_filter *filter); +struct odb_stream *odb_stream_from_object(struct object_database *odb, + const struct object_id *oid, + struct stream_filter *filter); /* - * Close the given read stream and release all resources associated with it. + * Create a new object stream for the given file descriptor. This can be used + * to, for example, stream an object into the object database. This function + * does _not_ take ownership of the file descriptor. It's the responsibility of + * the caller to close it after the stream has been closed. + */ +struct odb_stream *odb_stream_from_fd(int fd, size_t size, enum object_type type); + +/* + * Close the given object stream and release all resources associated with it. * Returns 0 on success, a negative error code otherwise. */ -int odb_read_stream_close(struct odb_read_stream *stream); +int odb_stream_close(struct odb_stream *stream); /* * Read data from the stream into the buffer. Returns 0 on EOF and the number * of bytes read on success. Returns a negative error code in case reading from * the stream fails. */ -ssize_t odb_read_stream_read(struct odb_read_stream *stream, void *buf, size_t len); - -/* - * A stream that provides an object to be written to the object database without - * loading all of it into memory. - */ -struct odb_write_stream { - ssize_t (*read)(struct odb_write_stream *, unsigned char *, size_t); - void *data; - int is_finished; -}; - -/* - * Read data from the stream into the buffer. Returns 0 when finished and the - * number of bytes read on success. Returns a negative error code in case - * reading from the stream fails. - */ -ssize_t odb_write_stream_read(struct odb_write_stream *stream, void *buf, - size_t len); - -/* - * Releases memory allocated for underlying stream data. - */ -void odb_write_stream_release(struct odb_write_stream *stream); +ssize_t odb_stream_read(struct odb_stream *stream, void *buf, size_t len); /* * Look up the object by its ID and write the full contents to the file @@ -88,10 +73,4 @@ int odb_stream_blob_to_fd(struct object_database *odb, struct stream_filter *filter, int can_seek); -/* - * Sets up an ODB write stream that reads from an fd. - */ -void odb_write_stream_from_fd(struct odb_write_stream *stream, int fd, - size_t size); - #endif /* STREAMING_H */ diff --git a/odb/transaction.c b/odb/transaction.c index dab7da6a9a4f55..69d71b9e97c61d 100644 --- a/odb/transaction.c +++ b/odb/transaction.c @@ -39,10 +39,10 @@ int odb_transaction_commit(struct odb_transaction *transaction) } int odb_transaction_write_object_stream(struct odb_transaction *transaction, - struct odb_write_stream *stream, - size_t len, struct object_id *oid) + struct odb_stream *stream, + struct object_id *oid) { - return transaction->write_object_stream(transaction, stream, len, oid); + return transaction->write_object_stream(transaction, stream, oid); } int odb_transaction_env(struct odb_transaction *transaction, struct strvec *env) diff --git a/odb/transaction.h b/odb/transaction.h index 4cb2eafcbf08f5..65248a409c820d 100644 --- a/odb/transaction.h +++ b/odb/transaction.h @@ -24,14 +24,14 @@ struct odb_transaction { /* * This callback is expected to write the given object stream into - * the ODB transaction. Note that for now, only blobs support streaming. + * the ODB transaction. * * The resulting object ID shall be written into the out pointer. The * callback is expected to return 0 on success, a negative error code * otherwise. */ int (*write_object_stream)(struct odb_transaction *transaction, - struct odb_write_stream *stream, size_t len, + struct odb_stream *stream, struct object_id *oid); /* @@ -81,8 +81,8 @@ int odb_transaction_commit(struct odb_transaction *transaction); * error code otherwise. */ int odb_transaction_write_object_stream(struct odb_transaction *transaction, - struct odb_write_stream *stream, - size_t len, struct object_id *oid); + struct odb_stream *stream, + struct object_id *oid); /* * Populates the provided strvec with the environment variables that a child diff --git a/oss-fuzz/fuzz-pack-headers.c b/oss-fuzz/fuzz-pack-headers.c index ef61ab577c5098..e44afe0b8d232b 100644 --- a/oss-fuzz/fuzz-pack-headers.c +++ b/oss-fuzz/fuzz-pack-headers.c @@ -9,7 +9,7 @@ int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) size_t len; unpack_object_header_buffer((const unsigned char *)data, - (unsigned long)size, &type, &len); + size, &type, &len); return 0; } diff --git a/pack-check.c b/pack-check.c index c3b8db7c5c41a6..c7275f4b9282f5 100644 --- a/pack-check.c +++ b/pack-check.c @@ -34,7 +34,7 @@ int check_pack_crc(struct packed_git *p, struct pack_window **w_curs, uint32_t data_crc = crc32(0, NULL, 0); do { - unsigned long avail; + size_t avail; void *data = use_pack(p, w_curs, offset, &avail); if (avail > len) avail = len; @@ -71,7 +71,7 @@ static int verify_packfile(struct repository *r, git_hash_init(&ctx, r->hash_algo); do { - unsigned long remaining; + size_t remaining; unsigned char *in = use_pack(p, w_curs, offset, &remaining); offset += remaining; if (!pack_sig_ofs) @@ -106,7 +106,7 @@ static int verify_packfile(struct repository *r, QSORT(entries, nr_objects, compare_entries); for (i = 0; i < nr_objects; i++) { - struct odb_read_stream *stream = NULL; + struct odb_stream *stream = NULL; void *data; struct object_id oid; enum object_type type; @@ -171,7 +171,7 @@ static int verify_packfile(struct repository *r, display_progress(progress, base_count + i); if (stream) - odb_read_stream_close(stream); + odb_stream_close(stream); free(data); } diff --git a/packfile-list.c b/packfile-list.c index 01fb913abf78fc..d6d411823c34a9 100644 --- a/packfile-list.c +++ b/packfile-list.c @@ -57,11 +57,12 @@ void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack) list->tail = entry; } -void packfile_list_append(struct packfile_list *list, struct packed_git *pack) +void packfile_list_append(struct packfile_list *list, struct packed_git *pack, + int skip_dup_check) { struct packfile_list_entry *entry; - entry = packfile_list_remove_internal(list, pack); + entry = skip_dup_check ? NULL : packfile_list_remove_internal(list, pack); if (!entry) { entry = xmalloc(sizeof(*entry)); entry->pack = pack; diff --git a/packfile-list.h b/packfile-list.h index 1b05e2aa36de08..2b4b98b22673a4 100644 --- a/packfile-list.h +++ b/packfile-list.h @@ -15,7 +15,8 @@ struct packfile_list_entry { void packfile_list_clear(struct packfile_list *list); void packfile_list_remove(struct packfile_list *list, struct packed_git *pack); void packfile_list_prepend(struct packfile_list *list, struct packed_git *pack); -void packfile_list_append(struct packfile_list *list, struct packed_git *pack); +void packfile_list_append(struct packfile_list *list, struct packed_git *pack, + int skip_dup_check); /* * Find the pack within the "packs" list whose index contains the object diff --git a/packfile.c b/packfile.c index 0eee45055f833e..2ec5d221b4c088 100644 --- a/packfile.c +++ b/packfile.c @@ -620,7 +620,7 @@ static int in_window(struct repository *r, struct pack_window *win, unsigned char *use_pack(struct packed_git *p, struct pack_window **w_cursor, off_t offset, - unsigned long *left) + size_t *left) { struct pack_window *win = *w_cursor; @@ -781,7 +781,7 @@ void packfile_store_add_pack(struct odb_source_packed *store, if (pack->pack_fd != -1) pack_open_fds++; - packfile_list_append(&store->packs, pack); + packfile_list_append(&store->packs, pack, 1); strmap_put(&store->packs_by_path, pack->pack_name, pack); } @@ -866,12 +866,11 @@ struct packfile_list_entry *packfile_store_get_packs(struct odb_source_packed *s return store->packs.head; } -unsigned long unpack_object_header_buffer(const unsigned char *buf, - unsigned long len, enum object_type *type, size_t *sizep) +size_t unpack_object_header_buffer(const unsigned char *buf, size_t len, + enum object_type *type, size_t *sizep) { unsigned shift; - size_t size, c; - unsigned long used = 0; + size_t size, c, used = 0; c = buf[used++]; *type = (c >> 4) & 7; @@ -960,8 +959,7 @@ int unpack_object_header(struct packed_git *p, size_t *sizep) { unsigned char *base; - unsigned long left; - unsigned long used; + size_t left, used; enum object_type type; /* use_pack() assures us we have [base, base + 20) available @@ -2115,7 +2113,7 @@ int parse_pack_header_option(const char *in, unsigned char *out, unsigned int *l } struct odb_packed_read_stream { - struct odb_read_stream base; + struct odb_stream base; struct packed_git *pack; git_zstream z; enum { @@ -2127,7 +2125,7 @@ struct odb_packed_read_stream { off_t pos; }; -static ssize_t read_istream_pack_non_delta(struct odb_read_stream *_st, char *buf, +static ssize_t read_istream_pack_non_delta(struct odb_stream *_st, char *buf, size_t sz) { struct odb_packed_read_stream *st = (struct odb_packed_read_stream *)_st; @@ -2187,7 +2185,7 @@ static ssize_t read_istream_pack_non_delta(struct odb_read_stream *_st, char *bu return total_read; } -static int close_istream_pack_non_delta(struct odb_read_stream *_st) +static int close_istream_pack_non_delta(struct odb_stream *_st) { struct odb_packed_read_stream *st = (struct odb_packed_read_stream *)_st; if (st->z_state == ODB_PACKED_READ_STREAM_INUSE) @@ -2195,7 +2193,7 @@ static int close_istream_pack_non_delta(struct odb_read_stream *_st) return 0; } -int packfile_read_object_stream(struct odb_read_stream **out, +int packfile_read_object_stream(struct odb_stream **out, const struct object_id *oid, struct packed_git *pack, off_t offset) diff --git a/packfile.h b/packfile.h index e1f77152b5c4bf..c1e387d6350b42 100644 --- a/packfile.h +++ b/packfile.h @@ -12,7 +12,7 @@ /* in odb.h */ struct object_info; -struct odb_read_stream; +struct odb_stream; struct packed_git { struct pack_window *windows; @@ -240,7 +240,8 @@ uint32_t get_pack_fanout(struct packed_git *p, uint32_t value); struct object_database; -unsigned char *use_pack(struct packed_git *, struct pack_window **, off_t, unsigned long *); +unsigned char *use_pack(struct packed_git *, struct pack_window **, off_t, + size_t *); void close_pack_windows(struct packed_git *); void close_pack(struct packed_git *); void unuse_pack(struct pack_window **); @@ -299,14 +300,15 @@ int packfile_fill_entry(struct packed_git *p, int is_pack_valid(struct packed_git *); void *unpack_entry(struct repository *r, struct packed_git *, off_t, enum object_type *, size_t *); -unsigned long unpack_object_header_buffer(const unsigned char *buf, unsigned long len, enum object_type *type, size_t *sizep); +size_t unpack_object_header_buffer(const unsigned char *buf, size_t len, + enum object_type *type, size_t *sizep); size_t get_size_from_delta(struct packed_git *, struct pack_window **, off_t); int unpack_object_header(struct packed_git *, struct pack_window **, off_t *, size_t *); off_t get_delta_base(struct packed_git *p, struct pack_window **w_curs, off_t *curpos, enum object_type type, off_t delta_obj_offset); -int packfile_read_object_stream(struct odb_read_stream **out, +int packfile_read_object_stream(struct odb_stream **out, const struct object_id *oid, struct packed_git *pack, off_t offset); diff --git a/parse-options.c b/parse-options.c index 08c21d9fc0a585..4519ead9dc77b8 100644 --- a/parse-options.c +++ b/parse-options.c @@ -1414,6 +1414,8 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t if (opts->type == OPTION_SUBCOMMAND) continue; + if (!full && (opts->flags & PARSE_OPT_HIDDEN)) + continue; if (opts->type == OPTION_GROUP) { fputc('\n', outfile); need_newline = 0; @@ -1421,8 +1423,6 @@ static enum parse_opt_result usage_with_options_internal(struct parse_opt_ctx_t fprintf(outfile, "%s\n", _(opts->help)); continue; } - if (!full && (opts->flags & PARSE_OPT_HIDDEN)) - continue; if (need_newline) { fputc('\n', outfile); diff --git a/parse-options.h b/parse-options.h index 3ec8ba5cc83c60..d7f896a9337024 100644 --- a/parse-options.h +++ b/parse-options.h @@ -237,6 +237,11 @@ struct option { .type = OPTION_GROUP, \ .help = (h), \ } +#define OPT_HIDDEN_GROUP(h) { \ + .type = OPTION_GROUP, \ + .help = (h), \ + .flags = PARSE_OPT_HIDDEN, \ +} #define OPT_BIT(s, l, v, h, b) OPT_BIT_F(s, l, v, h, b, 0) #define OPT_BITOP(s, l, v, h, set, clear) { \ .type = OPTION_BITOP, \ diff --git a/parse.c b/parse.c index d77f28046a0916..266bbd539be8dc 100644 --- a/parse.c +++ b/parse.c @@ -134,6 +134,15 @@ int git_parse_ssize_t(const char *value, ssize_t *ret) return 1; } +int git_parse_size_t(const char *value, size_t *ret) +{ + uintmax_t tmp; + if (!git_parse_unsigned(value, &tmp, maximum_signed_value_of_type(size_t))) + return 0; + *ret = tmp; + return 1; +} + int git_parse_double(const char *value, double *ret) { char *end; diff --git a/parse.h b/parse.h index a6dd37c4cba273..db742f35fb0e8e 100644 --- a/parse.h +++ b/parse.h @@ -4,6 +4,7 @@ int git_parse_signed(const char *value, intmax_t *ret, intmax_t max); int git_parse_unsigned(const char *value, uintmax_t *ret, uintmax_t max); int git_parse_ssize_t(const char *, ssize_t *); +int git_parse_size_t(const char *, size_t *); int git_parse_ulong(const char *, unsigned long *); int git_parse_uint(const char *value, unsigned int *ret); int git_parse_int(const char *value, int *ret); diff --git a/protocol-caps.c b/protocol-caps.c index 02261be14d817a..27e0f85b100cb9 100644 --- a/protocol-caps.c +++ b/protocol-caps.c @@ -11,7 +11,8 @@ #include "strbuf.h" struct requested_info { - unsigned size : 1; + unsigned size:1; + unsigned type:1; }; /* @@ -73,15 +74,20 @@ static void send_info(struct repository *r, struct packet_writer *writer, if (info->size) packet_writer_write(writer, "size"); + if (info->type) + packet_writer_write(writer, "type"); + for_each_string_list_item (item, oid_str_list) { const char *oid_str = item->string; + enum object_type object_type; struct object_id oid; size_t object_size; if (get_oid_hex_algop(oid_str, &oid, r->hash_algo) < 0) { packet_writer_error( writer, - "object-info: protocol error, expected to get oid, not '%s'", + "object-info: protocol error, expected to get " + "oid, not '%s'", oid_str); continue; } @@ -93,7 +99,8 @@ static void send_info(struct repository *r, struct packet_writer *writer, * If an object is not recognized by the server append SP to * the response. */ - if (get_object_info(r->objects, &oid, &object_size) <= OBJ_NONE) { + object_type = get_object_info(r->objects, &oid, &object_size); + if (object_type <= OBJ_NONE) { strbuf_addstr(&send_buffer, " "); goto write; } @@ -103,6 +110,9 @@ static void send_info(struct repository *r, struct packet_writer *writer, (uintmax_t)object_size); } + if (info->type) + strbuf_addf(&send_buffer, " %s", type_name(object_type)); + write: packet_writer_write(writer, "%s", send_buffer.buf); strbuf_reset(&send_buffer); @@ -124,6 +134,11 @@ int cap_object_info(struct repository *r, struct packet_reader *request) continue; } + if (!strcmp("type", request->line)) { + info.type = 1; + continue; + } + if (parse_oid(request->line, &oid_str_list)) continue; diff --git a/ref-filter.c b/ref-filter.c index 29aca08ce7b333..bdf54f6f592499 100644 --- a/ref-filter.c +++ b/ref-filter.c @@ -2744,6 +2744,72 @@ static int filter_exclude_match(struct ref_filter *filter, const char *refname) return match_pattern(filter->exclude.v, refname, filter->ignore_case); } +static const char *short_upstream_name(const char *full_ref) +{ + const char *short_name = full_ref; + + if (!skip_prefix(short_name, "refs/heads/", &short_name)) + skip_prefix(short_name, "refs/remotes/", &short_name); + return short_name; +} + +/* + * Match the configured upstream of a branch against the registered + * --forked patterns. Exact patterns are compared against the full + * upstream refname so they are unambiguous; glob patterns are matched + * against the abbreviated upstream so that a glob such as origin/... + * works as typed. + */ +static int filter_forked_match(struct ref_filter *filter, const char *refname) +{ + const char *short_name; + struct branch *branch; + const char *upstream; + + if (!skip_prefix(refname, "refs/heads/", &short_name)) + return 0; + branch = branch_get(short_name); + if (!branch) + return 0; + upstream = branch_get_upstream(branch, NULL); + if (!upstream) + return 0; + + for (size_t i = 0; i < filter->forked.nr; i++) { + const char *pattern = filter->forked.v[i]; + if (has_glob_specials(pattern)) { + if (!wildmatch(pattern, short_upstream_name(upstream), + WM_PATHNAME)) + return 1; + } else if (!strcmp(pattern, upstream)) { + return 1; + } + } + return 0; +} + +int ref_filter_forked_add(struct ref_filter *filter, const char *arg) +{ + struct object_id oid; + char *full_ref = NULL; + + if (has_glob_specials(arg)) { + strvec_push(&filter->forked, arg); + return 0; + } + + if (repo_dwim_ref(the_repository, arg, strlen(arg), &oid, + &full_ref, 0) == 1 && + (starts_with(full_ref, "refs/heads/") || + starts_with(full_ref, "refs/remotes/"))) { + strvec_push(&filter->forked, full_ref); + free(full_ref); + return 0; + } + free(full_ref); + return -1; +} + /* * We need to seek to the reference right after a given marker but excluding any * matching references. So we seek to the lexicographically next reference. @@ -2979,6 +3045,9 @@ static struct ref_array_item *apply_ref_filter(const struct reference *ref, if (filter->points_at.nr && !match_points_at(&filter->points_at, ref->oid, ref->name)) return NULL; + if (filter->forked.nr && !filter_forked_match(filter, ref->name)) + return NULL; + /* * A merge filter is applied on refs pointing to commits. Hence * obtain the commit using the 'oid' available and discard all @@ -3764,6 +3833,7 @@ void ref_filter_init(struct ref_filter *filter) void ref_filter_clear(struct ref_filter *filter) { strvec_clear(&filter->exclude); + strvec_clear(&filter->forked); oid_array_clear(&filter->points_at); commit_list_free(filter->with_commit); commit_list_free(filter->no_commit); diff --git a/ref-filter.h b/ref-filter.h index 120221b47fa30d..9361296e2a7440 100644 --- a/ref-filter.h +++ b/ref-filter.h @@ -67,6 +67,7 @@ struct ref_filter { const char **name_patterns; const char *start_after; struct strvec exclude; + struct strvec forked; struct oid_array points_at; struct commit_list *with_commit; struct commit_list *no_commit; @@ -110,6 +111,7 @@ struct ref_format { #define REF_FILTER_INIT { \ .points_at = OID_ARRAY_INIT, \ .exclude = STRVEC_INIT, \ + .forked = STRVEC_INIT, \ } #define REF_FORMAT_INIT { \ .use_color = GIT_COLOR_UNKNOWN, \ @@ -172,6 +174,14 @@ void ref_sorting_release(struct ref_sorting *); struct ref_sorting *ref_sorting_options(struct string_list *); /* Function to parse --merged and --no-merged options */ int parse_opt_merge_filter(const struct option *opt, const char *arg, int unset); +/* + * Register a --forked pattern on the filter. The argument is + * either a ref, which is resolved to its full refname, or a shell-style + * glob. Branches are kept only when their configured upstream matches + * one of the registered patterns. Returns -1 if the argument is not a + * valid ref or pattern. + */ +int ref_filter_forked_add(struct ref_filter *filter, const char *arg); /* Get the current HEAD's description */ char *get_head_description(void); /* Set up translated strings in the output. */ diff --git a/refs/files-backend.c b/refs/files-backend.c index 1cc20aa486c263..71628550f20a57 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -111,8 +111,7 @@ static void clear_loose_ref_cache(struct files_ref_store *refs) } } -static void files_ref_store_reparent(const char *name UNUSED, - const char *old_cwd, +static void files_ref_store_reparent(const char *old_cwd, const char *new_cwd, void *payload) { @@ -182,7 +181,7 @@ static struct ref_store *files_ref_store_init(struct repository *repo, packed_ref_store_init(repo, NULL, refs->gitcommondir, opts); refs->store_flags = opts->access_flags; - chdir_notify_register(NULL, files_ref_store_reparent, refs); + chdir_notify_register(files_ref_store_reparent, refs); strbuf_release(&refdir); @@ -234,7 +233,7 @@ static void files_ref_store_release(struct ref_store *ref_store) free(refs->gitcommondir); ref_store_release(refs->packed_ref_store); free(refs->packed_ref_store); - chdir_notify_unregister(NULL, files_ref_store_reparent, refs); + chdir_notify_unregister(files_ref_store_reparent, refs); } static void files_reflog_path(struct files_ref_store *refs, diff --git a/refs/packed-backend.c b/refs/packed-backend.c index b9b04b70105f9a..a73fc6aca78023 100644 --- a/refs/packed-backend.c +++ b/refs/packed-backend.c @@ -217,8 +217,7 @@ static size_t snapshot_hexsz(const struct snapshot *snapshot) return snapshot->refs->base.repo->hash_algo->hexsz; } -static void packed_ref_store_reparent(const char *name UNUSED, - const char *old_cwd, +static void packed_ref_store_reparent(const char *old_cwd, const char *new_cwd, void *payload) { @@ -248,7 +247,7 @@ struct ref_store *packed_ref_store_init(struct repository *repo, strbuf_addf(&sb, "%s/packed-refs", gitdir); refs->path = strbuf_detach(&sb, NULL); - chdir_notify_register(NULL, packed_ref_store_reparent, refs); + chdir_notify_register(packed_ref_store_reparent, refs); return ref_store; } @@ -293,7 +292,7 @@ static void packed_ref_store_release(struct ref_store *ref_store) clear_snapshot(refs); rollback_lock_file(&refs->lock); delete_tempfile(&refs->tempfile); - chdir_notify_unregister(NULL, packed_ref_store_reparent, refs); + chdir_notify_unregister(packed_ref_store_reparent, refs); free(refs->path); } diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 028f0211af3d0f..08a75fb3287502 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -391,8 +391,7 @@ static const struct reftable_be_write_options *reftable_be_write_options(struct return opts; } -static void reftable_be_reparent(const char *name UNUSED, - const char *old_cwd, +static void reftable_be_reparent(const char *old_cwd, const char *new_cwd, void *payload) { @@ -465,7 +464,7 @@ static struct ref_store *reftable_be_init(struct repository *repo, goto done; } - chdir_notify_register(NULL, reftable_be_reparent, refs); + chdir_notify_register(reftable_be_reparent, refs); done: assert(refs->err != REFTABLE_API_ERROR); @@ -492,7 +491,7 @@ static void reftable_be_release(struct ref_store *ref_store) free(be); } strmap_clear(&refs->worktree_backends, 0); - chdir_notify_unregister(NULL, reftable_be_reparent, refs); + chdir_notify_unregister(reftable_be_reparent, refs); } static int reftable_be_create_on_disk(struct ref_store *ref_store, diff --git a/reftable/block.c b/reftable/block.c index 1fa81405d2680f..de5af03c3b7bb3 100644 --- a/reftable/block.c +++ b/reftable/block.c @@ -87,7 +87,10 @@ int block_writer_init(struct block_writer *bw, uint8_t typ, uint8_t *block, REFTABLE_CALLOC_ARRAY(bw->zstream, 1); if (!bw->zstream) return REFTABLE_OUT_OF_MEMORY_ERROR; - deflateInit(bw->zstream, 9); + if (deflateInit(bw->zstream, 9) != Z_OK) { + REFTABLE_FREE_AND_NULL(bw->zstream); + return REFTABLE_ZLIB_ERROR; + } } return 0; diff --git a/reftable/writer.c b/reftable/writer.c index d969a6a0210e8b..073b9bbd8924ef 100644 --- a/reftable/writer.c +++ b/reftable/writer.c @@ -150,6 +150,7 @@ int reftable_writer_new(struct reftable_writer **out, { struct reftable_write_options opts = {0}; struct reftable_writer *wp; + int err; if (_opts) opts = *_opts; @@ -177,7 +178,12 @@ int reftable_writer_new(struct reftable_writer **out, wp->opts = opts; wp->hash_id = hash_id; wp->flush = flush_func; - writer_reinit_block_writer(wp, REFTABLE_BLOCK_TYPE_REF); + err = writer_reinit_block_writer(wp, REFTABLE_BLOCK_TYPE_REF); + if (err < 0) { + reftable_free(wp->block); + reftable_free(wp); + return err; + } *out = wp; diff --git a/repack-filtered.c b/repack-filtered.c index edcf7667c5c378..869b9fc6e3b94c 100644 --- a/repack-filtered.c +++ b/repack-filtered.c @@ -3,6 +3,12 @@ #include "repository.h" #include "run-command.h" #include "string-list.h" +#include "hex.h" +#include "packfile.h" +#include "list-objects-filter-options.h" +#include "list-objects-filter.h" +#include "odb.h" +#include "promisor-remote.h" int write_filtered_pack(const struct write_pack_opts *opts, struct existing_packs *existing, @@ -49,3 +55,79 @@ int write_filtered_pack(const struct write_pack_opts *opts, return finish_pack_objects_cmd(existing->repo->hash_algo, opts, &cmd, names); } + +struct collect_cb_data { + struct repository *repo; + struct oidset *set; +}; + +static int collect_promisor_blob(const struct object_id *oid, + struct object_info *oi UNUSED, + void *cb_data) +{ + struct collect_cb_data *data = cb_data; + struct object_info info = OBJECT_INFO_INIT; + enum object_type type; + + info.typep = &type; + + /* + * Use OBJECT_INFO_SKIP_FETCH_OBJECT to avoid triggering a + * lazy fetch while collecting promisor blobs. + */ + if (odb_read_object_info_extended(data->repo->objects, oid, &info, + OBJECT_INFO_SKIP_FETCH_OBJECT) < 0) + return 0; + + if (type == OBJ_BLOB) + oidset_insert(data->set, oid); + + return 0; +} + +int enumerate_promisor_blobs(struct repository *repo, + const struct list_objects_filter_options *filter, + struct oidset *to_drop) +{ + struct oidset all_promisor_blobs = OIDSET_INIT; + struct collect_cb_data cb = { + .repo = repo, + .set = &all_promisor_blobs + }; + int ret = 0; + + /* + * The caller (cmd_repack) is responsible for validating that a + * blob:limit filter and a promisor remote are present before + * calling this function. + * + * Walk only promisor objects. Every object visited here is a + * promisor object, so it is recoverable from the promisor remote + * as long as the remote still has it, the same assumption the rest + * of partial clone relies on. + * + * We do not use write_filtered_pack() here because git repack + * routes promisor objects through repack_promisor_objects() + * before the filter machinery runs, so the filtered pack never + * contains promisor blobs. Direct enumeration via + * ODB_FOR_EACH_OBJECT_PROMISOR_ONLY is the correct approach. + */ + ret = odb_for_each_object(repo->objects, NULL, + collect_promisor_blob, &cb, + ODB_FOR_EACH_OBJECT_PROMISOR_ONLY); + if (ret) + goto cleanup; + + /* + * Apply the filter to find which blobs exceed the threshold. + * The caller has to_drop and is responsible for clearing it. + */ + ret = list_objects_filter__filter_oidset(repo, + filter, + &all_promisor_blobs, + to_drop); + +cleanup: + oidset_clear(&all_promisor_blobs); + return ret; +} diff --git a/repack-promisor.c b/repack-promisor.c index 90318ce15093f5..fabfdc168a9b86 100644 --- a/repack-promisor.c +++ b/repack-promisor.c @@ -6,10 +6,12 @@ #include "path.h" #include "repository.h" #include "run-command.h" +#include "oidset.h" struct write_oid_context { struct child_process *cmd; const struct git_hash_algo *algop; + const struct oidset *to_drop; }; /* @@ -23,6 +25,15 @@ static int write_oid(const struct object_id *oid, struct write_oid_context *ctx = data; struct child_process *cmd = ctx->cmd; + /* + * Objects in to_drop are being removed from the repository, so + * omit them from the rebuilt promisor pack. Each such object is a + * promisor object and therefore remains recoverable from the + * promisor remote. + */ + if (ctx->to_drop && oidset_contains(ctx->to_drop, oid)) + return 0; + if (cmd->in == -1) { if (start_command(cmd)) die(_("could not start pack-objects to repack promisor objects")); @@ -81,7 +92,8 @@ static void finish_repacking_promisor_objects(struct repository *repo, void repack_promisor_objects(struct repository *repo, const struct pack_objects_args *args, - struct string_list *names, const char *packtmp) + struct string_list *names, const char *packtmp, + const struct oidset *to_drop) { struct write_oid_context ctx; struct child_process cmd = CHILD_PROCESS_INIT; @@ -98,6 +110,7 @@ void repack_promisor_objects(struct repository *repo, */ ctx.cmd = &cmd; ctx.algop = repo->hash_algo; + ctx.to_drop = to_drop; odb_for_each_object(repo->objects, NULL, write_oid, &ctx, ODB_FOR_EACH_OBJECT_PROMISOR_ONLY); diff --git a/repack.h b/repack.h index f9fbc895f02940..61e554e4ed383d 100644 --- a/repack.h +++ b/repack.h @@ -3,6 +3,7 @@ #include "list-objects-filter-options.h" #include "string-list.h" +#include "oidset.h" struct pack_objects_args { char *window; @@ -100,7 +101,8 @@ void generated_pack_install(struct generated_pack *pack, const char *name, void repack_promisor_objects(struct repository *repo, const struct pack_objects_args *args, - struct string_list *names, const char *packtmp); + struct string_list *names, const char *packtmp, + const struct oidset *to_drop); struct pack_geometry { struct packed_git **pack; @@ -165,6 +167,10 @@ int write_filtered_pack(const struct write_pack_opts *opts, struct existing_packs *existing, struct string_list *names); +int enumerate_promisor_blobs(struct repository *repo, + const struct list_objects_filter_options *filter, + struct oidset *to_drop); + int write_cruft_pack(const struct write_pack_opts *opts, const char *cruft_expiration, unsigned long combine_cruft_below_size, diff --git a/replay.c b/replay.c index 463c900d6c7c56..1e1bc7c10a8ffe 100644 --- a/replay.c +++ b/replay.c @@ -254,9 +254,9 @@ static void set_up_replay_mode(struct repository *repo, strset_clear(&rinfo.positive_refs); } -static struct commit *mapped_commit(kh_oid_map_t *replayed_commits, - struct commit *commit, - struct commit *fallback) +static struct commit *get_mapped_commit(kh_oid_map_t *replayed_commits, + struct commit *commit, + struct commit *fallback) { khint_t pos; if (!commit) @@ -267,27 +267,36 @@ static struct commit *mapped_commit(kh_oid_map_t *replayed_commits, return kh_value(replayed_commits, pos); } +static void put_mapped_commit(kh_oid_map_t *replayed_commits, + struct commit *commit, + struct commit *new_commit) +{ + khint_t pos; + int ret; + + pos = kh_put_oid_map(replayed_commits, commit->object.oid, &ret); + if (ret == 0) + BUG("Duplicate rewritten commit: %s", + oid_to_hex(&commit->object.oid)); + + kh_value(replayed_commits, pos) = new_commit; +} + static struct commit *pick_regular_commit(struct repository *repo, struct commit *pickme, - kh_oid_map_t *replayed_commits, - struct commit *onto, + struct commit *replayed_base, struct merge_options *merge_opt, struct merge_result *result, enum replay_mode mode, enum replay_empty_commit_action empty) { - struct commit *base, *replayed_base; struct tree *pickme_tree, *base_tree, *replayed_base_tree; - if (pickme->parents) { - base = pickme->parents->item; - base_tree = repo_get_commit_tree(repo, base); - } else { - base = NULL; + if (pickme->parents) + base_tree = repo_get_commit_tree(repo, pickme->parents->item); + else base_tree = lookup_tree(repo, repo->hash_algo->empty_tree); - } - replayed_base = mapped_commit(replayed_commits, base, onto); replayed_base_tree = repo_get_commit_tree(repo, replayed_base); pickme_tree = repo_get_commit_tree(repo, pickme); @@ -395,6 +404,12 @@ int replay_revisions(struct rev_info *revs, set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &detached_head, &advance, &revert, &onto, &update_refs); + if (opts->linearize && + update_refs && strset_get_size(update_refs) > 1) { + ret = error(_("'--linearize' cannot be used with multiple revision ranges")); + goto out; + } + if (opts->ref) { struct object_id oid; @@ -427,24 +442,46 @@ int replay_revisions(struct rev_info *revs, replayed_commits = kh_init_oid_map(); while ((commit = get_revision(revs))) { const struct name_decoration *decoration; - khint_t pos; - int hr; - if (commit->parents && commit->parents->next) - die(_("replaying merge commits is not supported yet!")); + if (commit->parents && commit->parents->next) { + if (!opts->linearize) + die(_("replaying merge commits is not supported yet!")); + /* + * Drop the merge commit: do not pick it, leave + * `last_commit` unchanged, and fall through to the + * rest of the loop. As a result: + * - refs pointing to the merge commit will be updated + * to `last_commit`. + * - the next replayed commit uses `last_commit` as its + * `base`. + */ + } else { + /* + * Decide where to replay this commit onto. + * If the parent commit was replayed already, the replayed result + * can be found in `replayed_commits`. Otherwise fall back to `onto`. + * When reverting, commits are replayed in reverse order and thus + * its parent isn't replayed yet. Therefore revert commits are + * always replayed onto `last_commit`. + * Also when opts->linearize is true, set the base to + * `last_commit` to create a single linear history. + */ + struct commit *parent = commit->parents ? commit->parents->item : NULL; + struct commit *base = get_mapped_commit(replayed_commits, parent, onto); + + if (opts->linearize || mode == REPLAY_MODE_REVERT) + base = last_commit; + + last_commit = pick_regular_commit(revs->repo, commit, base, + &merge_opt, &result, + mode, opts->empty); + } - last_commit = pick_regular_commit(revs->repo, commit, replayed_commits, - mode == REPLAY_MODE_REVERT ? last_commit : onto, - &merge_opt, &result, mode, opts->empty); if (!last_commit) break; /* Record commit -> last_commit mapping */ - pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr); - if (hr == 0) - BUG("Duplicate rewritten commit: %s\n", - oid_to_hex(&commit->object.oid)); - kh_value(replayed_commits, pos) = last_commit; + put_mapped_commit(replayed_commits, commit, last_commit); /* Update any necessary branches */ if (ref) diff --git a/replay.h b/replay.h index 491db145e30151..2c71afbfde05b4 100644 --- a/replay.h +++ b/replay.h @@ -62,6 +62,11 @@ struct replay_revisions_options { * Defaults to REPLAY_EMPTY_COMMIT_DROP. */ enum replay_empty_commit_action empty; + + /* + * Whether to linearize the commits (i.e. drop merge commits). + */ + int linearize; }; /* This struct is used as an out-parameter by `replay_revisions()`. */ diff --git a/repository.c b/repository.c index 651b0f69339452..2d66cfdbd55994 100644 --- a/repository.c +++ b/repository.c @@ -74,6 +74,7 @@ void initialize_repository(struct repository *repo) index_state_init(repo->index, repo); repo->check_deprecated_config = true; repo->bare_cfg = -1; + repo->fetch_if_missing = 1; repo_config_values_init(&repo->config_values_private_); /* @@ -201,8 +202,6 @@ void repo_set_compat_hash_algo(struct repository *repo MAYBE_UNUSED, uint32_t al if (hash_algo_by_ptr(repo->hash_algo) == algo) BUG("hash_algo and compat_hash_algo match"); repo->compat_hash_algo = algo ? &hash_algos[algo] : NULL; - if (repo->compat_hash_algo) - repo_read_loose_object_map(repo); #else if (algo) die(_("compatibility hash algorithm support requires Rust")); @@ -296,6 +295,7 @@ int repo_init(struct repository *repo, warning("%s", err.buf); goto error; } + repo->objects = odb_new(repo, 0); if (worktree) repo_set_worktree(repo, worktree); diff --git a/repository.h b/repository.h index 3b467a25133670..11f5c2ed10dc3f 100644 --- a/repository.h +++ b/repository.h @@ -184,6 +184,12 @@ struct repository { /* True if commit-graph has been disabled within this process. */ int commit_graph_disabled; + /* + * Controls whether the repository should lazily fetch missing + * objects from promisor remotes. Defaults to 1. + */ + int fetch_if_missing; + /* * Lazily-populated cache mapping hook event names to configured hooks. * NULL until first hook use. diff --git a/revision.c b/revision.c index 50dc8b199137c7..35afe52208e710 100644 --- a/revision.c +++ b/revision.c @@ -2732,7 +2732,7 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg revs->ignore_missing = 1; } else if (opt && opt->allow_exclude_promisor_objects && !strcmp(arg, "--exclude-promisor-objects")) { - if (fetch_if_missing) + if (revs->repo->fetch_if_missing) BUG("exclude_promisor_objects can only be used when fetch_if_missing is 0"); revs->exclude_promisor_objects = 1; } else { diff --git a/sequencer.c b/sequencer.c index 83c38492052b46..4341136d4fdd9a 100644 --- a/sequencer.c +++ b/sequencer.c @@ -1127,6 +1127,7 @@ static int run_git_commit(const char *defmsg, struct child_process cmd = CHILD_PROCESS_INIT; cmd.git_cmd = 1; + cmd.odb_to_close = the_repository->objects; if (is_rebase_i(opts) && ((opts->committer_date_is_author_date && !opts->ignore_date) || @@ -4796,7 +4797,8 @@ void create_autostash_ref(struct repository *r, const char *refname, static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply, const char *label_ours, const char *label_theirs, const char *label_base, - const char *stash_msg) + const char *stash_msg, + bool *conflicted) { struct child_process child = CHILD_PROCESS_INIT; int ret = 0; @@ -4831,14 +4833,16 @@ static int apply_save_autostash_oid(const char *stash_oid, int attempt_apply, strvec_push(&store.args, stash_oid); if (run_command(&store)) ret = error(_("cannot store %s"), stash_oid); - else if (attempt_apply) + else if (attempt_apply) { + if (conflicted) + *conflicted = true; fprintf(stderr, _("Your local changes are stashed, however applying them\n" "resulted in conflicts. You can either resolve the conflicts\n" "and then discard the stash with \"git stash drop\", or, if you\n" "do not want to resolve them now, run \"git reset --hard\" and\n" "apply the local changes later by running \"git stash pop\".\n")); - else + } else fprintf(stderr, _("Autostash exists; creating a new stash entry.\n" "Your changes are safe in the stash.\n" @@ -4862,7 +4866,7 @@ static int apply_save_autostash(const char *path, int attempt_apply) strbuf_trim(&stash_oid); ret = apply_save_autostash_oid(stash_oid.buf, attempt_apply, - NULL, NULL, NULL, NULL); + NULL, NULL, NULL, NULL, NULL); unlink(path); strbuf_release(&stash_oid); @@ -4881,19 +4885,24 @@ int apply_autostash(const char *path) int apply_autostash_oid(const char *stash_oid) { - return apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL); + return apply_save_autostash_oid(stash_oid, 1, NULL, NULL, NULL, NULL, + NULL); } static int apply_save_autostash_ref(struct repository *r, const char *refname, int attempt_apply, const char *label_ours, const char *label_theirs, const char *label_base, - const char *stash_msg) + const char *stash_msg, + bool *conflicted) { struct object_id stash_oid; char stash_oid_hex[GIT_MAX_HEXSZ + 1]; int flag, ret; + if (conflicted) + *conflicted = false; + if (!refs_ref_exists(get_main_ref_store(r), refname)) return 0; @@ -4906,7 +4915,7 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname, oid_to_hex_r(stash_oid_hex, &stash_oid); ret = apply_save_autostash_oid(stash_oid_hex, attempt_apply, label_ours, label_theirs, label_base, - stash_msg); + stash_msg, conflicted); refs_delete_ref(get_main_ref_store(r), "", refname, &stash_oid, REF_NO_DEREF); @@ -4917,16 +4926,17 @@ static int apply_save_autostash_ref(struct repository *r, const char *refname, int save_autostash_ref(struct repository *r, const char *refname) { return apply_save_autostash_ref(r, refname, 0, - NULL, NULL, NULL, NULL); + NULL, NULL, NULL, NULL, NULL); } int apply_autostash_ref(struct repository *r, const char *refname, const char *label_ours, const char *label_theirs, - const char *label_base, const char *stash_msg) + const char *label_base, const char *stash_msg, + bool *conflicted) { return apply_save_autostash_ref(r, refname, 1, label_ours, label_theirs, label_base, - stash_msg); + stash_msg, conflicted); } static int checkout_onto(struct repository *r, struct replay_opts *opts, @@ -6277,7 +6287,6 @@ int sequencer_make_script(struct repository *r, struct strbuf *out, revs.sort_order = REV_SORT_IN_GRAPH_ORDER; revs.topo_order = 1; - revs.pretty_given = 1; repo_config_get_string(the_repository, "rebase.instructionFormat", &format); if (!format || !*format) { free(format); diff --git a/sequencer.h b/sequencer.h index 64a9c7fb1beccd..b39528b6d0610c 100644 --- a/sequencer.h +++ b/sequencer.h @@ -237,7 +237,8 @@ int apply_autostash(const char *path); int apply_autostash_oid(const char *stash_oid); int apply_autostash_ref(struct repository *r, const char *refname, const char *label_ours, const char *label_theirs, - const char *label_base, const char *stash_msg); + const char *label_base, const char *stash_msg, + bool *conflicted); #define SUMMARY_INITIAL_COMMIT (1 << 0) #define SUMMARY_SHOW_AUTHOR_DATE (1 << 1) diff --git a/serve.c b/serve.c index 2b07d922b3dde1..1b4369fb683965 100644 --- a/serve.c +++ b/serve.c @@ -46,6 +46,9 @@ static int promisor_remote_advertise(struct repository *r, static void promisor_remote_receive(struct repository *r, const char *remotes) { + if (!remotes) + die("promisor-remote capability requires an argument"); + mark_promisor_remotes_as_accepted(r, remotes); } @@ -97,9 +100,9 @@ static int object_info_advertise(struct repository *r, struct strbuf *value) /* disabled by default */ advertise_object_info = 0; } - /* Currently only size is supported */ + /* Currently only size and type are supported */ if (value && advertise_object_info) - strbuf_addstr(value, "size"); + strbuf_addstr(value, "size type"); return advertise_object_info; } diff --git a/setup.c b/setup.c index 95909e96031262..22beb86f7acb1e 100644 --- a/setup.c +++ b/setup.c @@ -1057,8 +1057,7 @@ static void apply_gitdir_and_environment(struct repository *repo, const char *pa strvec_clear(&to_free); } -static void update_relative_gitdir(const char *name UNUSED, - const char *old_cwd, +static void update_relative_gitdir(const char *old_cwd, const char *new_cwd, void *data) { @@ -1086,7 +1085,7 @@ static void apply_and_export_relative_gitdir(struct repository *repo, const char xsetenv(GIT_DIR_ENVIRONMENT, path, 1); if (!is_absolute_path(path)) - chdir_notify_register(NULL, update_relative_gitdir, repo); + chdir_notify_register(update_relative_gitdir, repo); strbuf_release(&realpath); } @@ -1765,8 +1764,6 @@ int apply_repository_format(struct repository *repo, enum apply_repository_format_flags flags, struct strbuf *err) { - char *object_directory = NULL, *alternate_object_directories = NULL; - if (verify_repository_format(format, err) < 0) return -1; @@ -1779,8 +1776,6 @@ int apply_repository_format(struct repository *repo, if (flags & APPLY_REPOSITORY_FORMAT_HONOR_ENV) { const char *shallow_file; - object_directory = xstrdup_or_null(getenv(DB_ENVIRONMENT)); - alternate_object_directories = xstrdup_or_null(getenv(ALTERNATE_DB_ENVIRONMENT)); shallow_file = getenv(GIT_SHALLOW_FILE_ENVIRONMENT); if (shallow_file) set_alternate_shallow_file(repo, shallow_file); @@ -1788,8 +1783,6 @@ int apply_repository_format(struct repository *repo, repo->bare_cfg = format->is_bare; repo_set_hash_algo(repo, format->hash_algo); - repo->objects = odb_new(repo, object_directory, - alternate_object_directories); repo_set_compat_hash_algo(repo, format->compat_hash_algo); repo_set_ref_storage_format(repo, format->ref_storage_format, @@ -1805,8 +1798,6 @@ int apply_repository_format(struct repository *repo, repo->repository_format_precious_objects = format->precious_objects; - free(alternate_object_directories); - free(object_directory); return 0; } @@ -1890,6 +1881,7 @@ const char *enter_repo(struct repository *repo, const char *path, unsigned flags read_and_verify_repository_format(&fmt, ".", NULL); if (apply_repository_format(repo, &fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0) die("%s", err.buf); + repo->objects = odb_new(repo, ODB_NEW_HONOR_ENV); startup_info->have_repository = 1; clear_repository_format(&fmt); @@ -2092,6 +2084,7 @@ const char *setup_git_directory_gently(struct repository *repo, int *nongit_ok) if (apply_repository_format(repo, &discovery.format, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0) die("%s", err.buf); + repo->objects = odb_new(repo, ODB_NEW_HONOR_ENV); clear_repository_format(&discovery.format); strbuf_release(&err); @@ -2653,25 +2646,29 @@ static int create_default_files(struct repository *repo, return reinit; } -static void create_object_directory(struct repository *repo) +static void create_object_database(struct repository *repo) { - struct strbuf path = STRBUF_INIT; - size_t baselen; - - strbuf_addstr(&path, repo_get_object_directory(repo)); - baselen = path.len; - - safe_create_dir(repo, path.buf, 1); - - strbuf_setlen(&path, baselen); - strbuf_addstr(&path, "/pack"); - safe_create_dir(repo, path.buf, 1); + /* + * Create the "objects" directory in the common directory. This is done + * so that the repository can be discovered regardless of the backend + * used. + * + * Note that we only do this in case the object directory wasn't + * overwritten via an environment variable. If it _is_ being overridden + * then we skip this step, as the repository won't be discoverable + * anyway without the environment variable. + */ + if (!getenv(DB_ENVIRONMENT)) { + struct strbuf objects_dir = STRBUF_INIT; + repo_common_path_append(repo, &objects_dir, "objects"); + safe_create_dir(repo, objects_dir.buf, 1); + strbuf_release(&objects_dir); + } - strbuf_setlen(&path, baselen); - strbuf_addstr(&path, "/info"); - safe_create_dir(repo, path.buf, 1); + repo->objects = odb_new(repo, ODB_NEW_HONOR_ENV); - strbuf_release(&path); + if (odb_source_create_on_disk(repo->objects->sources) < 0) + die(_("failed creating object database")); } static void separate_git_dir(struct repository *repo, @@ -2869,7 +2866,6 @@ int init_db(struct repository *repo, repository_format_configure(&repo_fmt, hash, ref_storage_format); if (apply_repository_format(repo, &repo_fmt, APPLY_REPOSITORY_FORMAT_HONOR_ENV, &err) < 0) die("%s", err.buf); - startup_info->have_repository = 1; /* * Ensure `core.hidedotfiles` is processed. This must happen after we @@ -2885,7 +2881,9 @@ int init_db(struct repository *repo, if (!(flags & INIT_DB_SKIP_REFDB)) create_reference_database(repo, initial_branch, flags & INIT_DB_QUIET); - create_object_directory(repo); + create_object_database(repo); + + startup_info->have_repository = 1; if (repo_settings_get_shared_repository(repo)) { char buf[10]; diff --git a/setup.h b/setup.h index 654f10e059b995..763fd384e86c28 100644 --- a/setup.h +++ b/setup.h @@ -245,8 +245,8 @@ enum apply_repository_format_flags { /* * Apply the given repository format to the repo. This initializes extensions - * and basic data structures required for normal operation. Returns 0 on - * success, a negative error code when the format is not valid as determined by + * required for normal operation. Returns 0 on success, a negative error code + * when the format is not valid as determined by * `verify_repository_format()`. */ int apply_repository_format(struct repository *repo, diff --git a/sideband.c b/sideband.c index 4a2c5f858f60ad..374187402e5c5b 100644 --- a/sideband.c +++ b/sideband.c @@ -445,6 +445,7 @@ void send_sideband(int fd, int band, const char *data, ssize_t sz, int packet_ma const char *p = data; while (sz) { + struct iovec iov[2]; unsigned n; char hdr[5]; @@ -454,12 +455,19 @@ void send_sideband(int fd, int band, const char *data, ssize_t sz, int packet_ma if (0 <= band) { xsnprintf(hdr, sizeof(hdr), "%04x", n + 5); hdr[4] = band; - write_or_die(fd, hdr, 5); + iov[0].iov_base = hdr; + iov[0].iov_len = 5; } else { xsnprintf(hdr, sizeof(hdr), "%04x", n + 4); - write_or_die(fd, hdr, 4); + iov[0].iov_base = hdr; + iov[0].iov_len = 4; } - write_or_die(fd, p, n); + + iov[1].iov_base = (void *) p; + iov[1].iov_len = n; + + writev_or_die(fd, iov, ARRAY_SIZE(iov)); + p += n; sz -= n; } diff --git a/t/helper/test-delta.c b/t/helper/test-delta.c index 8223a60229229e..d807afef751b48 100644 --- a/t/helper/test-delta.c +++ b/t/helper/test-delta.c @@ -32,7 +32,7 @@ int cmd__delta(int argc, const char **argv) die_errno("unable to read '%s'", argv[3]); if (argv[1][1] == 'd') { - unsigned long delta_size; + size_t delta_size; out_buf = diff_delta(from.buf, from.len, data.buf, data.len, &delta_size, 0); diff --git a/t/helper/test-pack-deltas.c b/t/helper/test-pack-deltas.c index 840797cf0dbabb..959705fecaf144 100644 --- a/t/helper/test-pack-deltas.c +++ b/t/helper/test-pack-deltas.c @@ -22,7 +22,7 @@ static unsigned long do_compress(void **pptr, unsigned long size) { git_zstream stream; void *in, *out; - unsigned long maxsize; + size_t maxsize; git_deflate_init(&stream, 1); maxsize = git_deflate_bound(&stream, size); @@ -49,7 +49,7 @@ static void write_ref_delta(struct hashfile *f, { unsigned char header[MAX_PACK_OBJECT_HEADER]; unsigned long delta_size, compressed_size, hdrlen; - size_t size, base_size; + size_t size, base_size, delta_size_st = 0; enum object_type type; void *base_buf, *delta_buf; void *buf = odb_read_object(the_repository->objects, @@ -65,7 +65,8 @@ static void write_ref_delta(struct hashfile *f, die("unable to read %s", oid_to_hex(base)); delta_buf = diff_delta(base_buf, base_size, - buf, size, &delta_size, 0); + buf, size, &delta_size_st, 0); + delta_size = cast_size_t_to_ulong(delta_size_st); compressed_size = do_compress(&delta_buf, delta_size); diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c index 68579d83f3939e..f181f0c02d355a 100644 --- a/t/helper/test-parse-options.c +++ b/t/helper/test-parse-options.c @@ -209,6 +209,10 @@ int cmd__parse_options(int argc, const char **argv) OPT_GROUP("Alias"), OPT_STRING('A', "alias-source", &string, "string", "get a string"), OPT_ALIAS('Z', "alias-target", "alias-source"), + OPT_HIDDEN_GROUP("Hidden options"), + OPT_HIDDEN_BOOL(0, "hidden-bool", &boolean, "get a boolean"), + OPT_INTEGER_F('k', "hidden-integer", &integer, "get a integer", + PARSE_OPT_HIDDEN), OPT_END(), }; int ret = 0; diff --git a/t/meson.build b/t/meson.build index 2133c840da63dc..7f53cca7d1f891 100644 --- a/t/meson.build +++ b/t/meson.build @@ -796,6 +796,7 @@ integration_tests = [ 't6041-bisect-submodule.sh', 't6050-replace.sh', 't6060-merge-index.sh', + 't6099-merge-base-side-exhaustion.sh', 't6100-rev-list-in-order.sh', 't6101-rev-parse-parents.sh', 't6102-rev-list-unexpected-objects.sh', @@ -965,6 +966,7 @@ integration_tests = [ 't7703-repack-geometric.sh', 't7704-repack-cruft.sh', 't7705-repack-incremental-midx.sh', + 't7706-repack-drop-filtered.sh', 't7800-difftool.sh', 't7810-grep.sh', 't7811-grep-open.sh', diff --git a/t/perf/p5303-many-packs.sh b/t/perf/p5303-many-packs.sh index af173a7b73e398..4221f9dd706243 100755 --- a/t/perf/p5303-many-packs.sh +++ b/t/perf/p5303-many-packs.sh @@ -141,4 +141,8 @@ test_perf "load 10,000 packs" ' git rev-parse --verify "HEAD^{commit}" ' +test_perf "abbreviate with 10,000 packs" ' + git rev-parse --short HEAD +' + test_done diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh index a22533f9ed6d16..449fff4d34b172 100755 --- a/t/t0040-parse-options.sh +++ b/t/t0040-parse-options.sh @@ -7,7 +7,7 @@ test_description='our own option parser' . ./test-lib.sh -cat >expect <<\EOF +cat >expect-part1 <<\EOF usage: test-tool parse-options A helper function for the parse-options API. @@ -41,6 +41,9 @@ String options --[no-]string2 get another string --[no-]st get another string (pervert ordering) -o get another string +EOF + +cat >expect-part2 <<\EOF --longhelp help text of this entry spans multiple lines --[no-]list add str to list @@ -67,12 +70,32 @@ Alias EOF +cat >expect-noop <<\EOF + --[no-]obsolete no-op (backward compatibility) +EOF + +cat >expect-hidden <<\EOF +Hidden options + --[no-]hidden-bool get a boolean + -k, --[no-]hidden-integer + get a integer + +EOF + test_expect_success 'test help' ' + cat expect-part1 expect-part2 >expect && test-tool parse-options -h >output 2>output.err && test_must_be_empty output.err && test_cmp expect output ' +test_expect_success 'test --help-all shows hidden group and options' ' + cat expect-part1 expect-noop expect-part2 expect-hidden >expect-help-all && + test-tool parse-options --help-all >output 2>output.err && + test_must_be_empty output.err && + test_cmp expect-help-all output +' + mv expect expect.err check () { diff --git a/t/t0041-usage.sh b/t/t0041-usage.sh index 51af7cc0300efb..2a9c5eafcac2a6 100755 --- a/t/t0041-usage.sh +++ b/t/t0041-usage.sh @@ -107,4 +107,11 @@ test_expect_success 'for-each-ref usage error' ' test_grep "usage" actual.err ' +test_expect_success 'git --shallow-file without a value' ' + test_must_fail git --shallow-file >actual 2>actual.err && + test_line_count = 0 actual && + test_grep "no file given for " actual.err && + test_grep "usage" actual.err +' + test_done diff --git a/t/t0060-path-utils.sh b/t/t0060-path-utils.sh index 8545cdfab559b4..56faf5fe732ee0 100755 --- a/t/t0060-path-utils.sh +++ b/t/t0060-path-utils.sh @@ -602,7 +602,8 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD 'RUNTIME_PREFIX wor echo "echo HERE" | write_script pretend/libexec/git-core/git-here && GIT_EXEC_PATH= ./pretend/bin/git here >actual && echo HERE >expect && - test_cmp expect actual' + test_cmp expect actual +' test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works' ' git config yes.path "%(prefix)/yes" && @@ -611,4 +612,34 @@ test_expect_success !VALGRIND,RUNTIME_PREFIX,CAN_EXEC_IN_PWD '%(prefix)/ works' test_cmp expect actual ' +test_expect_success MINGW,RUNTIME_PREFIX 'MSYSTEM/PATH is adjusted if necessary' ' + if test -z "$MINGW_PREFIX" + then + MINGW_PREFIX="/$(echo "${MSYSTEM:-MINGW64}" | tr A-Z a-z)" + fi && + mkdir -p "$HOME"/bin pretend"$MINGW_PREFIX"/bin \ + pretend"$MINGW_PREFIX"/libexec/git-core pretend/usr/bin && + cp "$GIT_EXEC_PATH"/git.exe pretend"$MINGW_PREFIX"/bin/ && + cp "$GIT_EXEC_PATH"/git.exe pretend"$MINGW_PREFIX"/libexec/git-core/ && + # copy the .dll files, if any (happens when building via CMake) + if test -n "$(ls "$GIT_EXEC_PATH"/*.dll 2>/dev/null)" + then + cp "$GIT_EXEC_PATH"/*.dll pretend"$MINGW_PREFIX"/bin/ && + cp "$GIT_EXEC_PATH"/*.dll pretend"$MINGW_PREFIX"/libexec/git-core/ + fi && + echo "env | grep MSYSTEM=" | write_script "$HOME"/bin/git-test-home && + echo "echo ${MINGW_PREFIX#/}" | write_script pretend"$MINGW_PREFIX"/bin/git-test-bin && + echo "echo usr" | write_script pretend/usr/bin/git-test-bin2 && + + ( + MSYSTEM= && + GIT_EXEC_PATH= && + pretend"$MINGW_PREFIX"/libexec/git-core/git.exe test-home >actual && + pretend"$MINGW_PREFIX"/libexec/git-core/git.exe test-bin >>actual && + pretend"$MINGW_PREFIX"/bin/git.exe test-bin2 >>actual + ) && + test_write_lines MSYSTEM=$MSYSTEM "${MINGW_PREFIX#/}" usr >expect && + test_cmp expect actual +' + test_done diff --git a/t/t0450/adoc-help-mismatches b/t/t0450/adoc-help-mismatches index c4a55ff4e35a4f..baf3b1d80927d4 100644 --- a/t/t0450/adoc-help-mismatches +++ b/t/t0450/adoc-help-mismatches @@ -12,7 +12,6 @@ column credential credential-cache credential-store -fast-import fetch-pack fmt-merge-msg format-patch diff --git a/t/t1016-compatObjectFormat.sh b/t/t1016-compatObjectFormat.sh index 92d48b96a10932..9cafcee5098692 100755 --- a/t/t1016-compatObjectFormat.sh +++ b/t/t1016-compatObjectFormat.sh @@ -187,6 +187,24 @@ do eval signedtag3_${hash}_oid=$(git hash-object -t tag -w ../${hash}_signedtag3) && eval signedtag4_${hash}_oid=$(git hash-object -t tag -w ../${hash}_signedtag4) ' + + test_expect_success 'rev-parse maps oid of object borrowed from alternate' ' + for repo in alt borrow + do + test_when_finished "rm -rf $repo" && + git init --object-format=$hash $repo && + git -C $repo config set core.repositoryformatversion 1 && + git -C $repo config set extensions.compatObjectFormat $(compat_hash $hash) || exit 1 + done && + + git -C alt commit --allow-empty --message A && + echo "$(pwd)/alt/.git/objects" >borrow/.git/objects/info/alternates && + + oid=$(git -C alt rev-parse HEAD) && + git -C alt rev-parse --output-object-format=$(compat_hash $hash) "$oid" >expect && + git -C borrow rev-parse --output-object-format=$(compat_hash $hash) "$oid" >actual && + test_cmp expect actual + ' done cd "$base" diff --git a/t/t1017-cat-file-remote-object-info.sh b/t/t1017-cat-file-remote-object-info.sh index 116862f9d0b447..e2919aa061830a 100755 --- a/t/t1017-cat-file-remote-object-info.sh +++ b/t/t1017-cat-file-remote-object-info.sh @@ -7,6 +7,7 @@ test_description='git cat-file --batch-command with remote-object-info command' hello_content="Hello World" hello_size=$(strlen "$hello_content") +hello_type="blob" hello_oid=$(echo_without_newline "$hello_content" | git hash-object --stdin) hello_short_oid=$(git rev-parse --short "$hello_oid") @@ -19,6 +20,7 @@ unstored_oid=$(echo_without_newline "$unstored_content" | git hash-object --stdi # file name is hello, which is 5 characters # a space is 1 character and a null is 1 character tree_size=$(($(test_oid rawsz) + 13)) +tree_type="tree" commit_message="Initial commit" @@ -31,6 +33,7 @@ commit_message="Initial commit" # An easier way to calculate is: 1. use `git cat-file commit | wc -c`, # to get 177, 2. then deduct 40 hex characters to get 137 commit_size=$(($(test_oid hexsz) + 137)) +commit_type="commit" tag_header_without_oid="type blob tag hellotag @@ -44,6 +47,7 @@ $tag_description" tag_oid=$(echo_without_newline "$tag_content" | git hash-object -t tag --stdin -w) tag_size=$(strlen "$tag_content") +tag_type="tag" set_transport_variables () { hello_oid=$(echo_without_newline "$hello_content" | git hash-object --stdin) @@ -135,10 +139,10 @@ test_expect_success 'batch-command remote-object-info git:// default filter' ' set_transport_variables "$daemon_parent" && cd "$daemon_parent/daemon_client_empty" && - echo "$hello_oid $hello_size" >expect && - echo "$tree_oid $tree_size" >>expect && - echo "$commit_oid $commit_size" >>expect && - echo "$tag_oid $tag_size" >>expect && + echo "$hello_oid $hello_type $hello_size" >expect && + echo "$tree_oid $tree_type $tree_size" >>expect && + echo "$commit_oid $commit_type $commit_size" >>expect && + echo "$tag_oid $tag_type $tag_size" >>expect && git cat-file --batch-command >actual <<-EOF && remote-object-info "$GIT_DAEMON_URL/parent" $hello_oid $tree_oid @@ -148,7 +152,7 @@ test_expect_success 'batch-command remote-object-info git:// default filter' ' ) ' -test_expect_success 'remote-object-info does not change the default format of info' ' +test_expect_success 'remote-object-info and info can be mixed using the unified default format' ' ( set_transport_variables "$daemon_parent" && cd "$daemon_parent/daemon_client_empty" && @@ -158,7 +162,7 @@ test_expect_success 'remote-object-info does not change the default format of in local_size=$(strlen "$local_content") && echo "$local_oid blob $local_size" >expect && - echo "$hello_oid $hello_size" >>expect && + echo "$hello_oid blob $hello_size" >>expect && echo "$local_oid blob $local_size" >>expect && git cat-file --batch-command >actual <<-EOF && @@ -205,10 +209,10 @@ test_expect_success 'batch-command -Z remote-object-info git:// default filter' set_transport_variables "$daemon_parent" && cd "$daemon_parent/daemon_client_empty" && - printf "%s\0" "$hello_oid $hello_size" >expect && - printf "%s\0" "$tree_oid $tree_size" >>expect && - printf "%s\0" "$commit_oid $commit_size" >>expect && - printf "%s\0" "$tag_oid $tag_size" >>expect && + printf "%s\0" "$hello_oid $hello_type $hello_size" >expect && + printf "%s\0" "$tree_oid $tree_type $tree_size" >>expect && + printf "%s\0" "$commit_oid $commit_type $commit_size" >>expect && + printf "%s\0" "$tag_oid $tag_type $tag_size" >>expect && printf "%s\0" "$hello_oid missing" >>expect && printf "%s\0" "$tree_oid missing" >>expect && @@ -256,14 +260,12 @@ test_expect_success 'remote-object-info does not die on missing oid like info' ' ) ' -# This tests depends on %(objecttype) not being supported yet, once supported -# it needs to be updated. -test_expect_success 'unsupported placeholder on remote returns empty string' ' +test_expect_success 'objecttype is supported by remote-object-info' ' ( set_transport_variables "$daemon_parent" && cd "$daemon_parent/daemon_client_empty" && - echo "" >expect && + echo "$hello_type" >expect && git cat-file --batch-command="%(objecttype)" >actual <<-EOF && remote-object-info "$GIT_DAEMON_URL/parent" $hello_oid EOF @@ -271,6 +273,22 @@ test_expect_success 'unsupported placeholder on remote returns empty string' ' ) ' +test_expect_success 'unsupported placeholders on remote return empty string' ' + ( + set_transport_variables "$daemon_parent" && + cd "$daemon_parent/daemon_client_empty" && + + fmt="%(objectmode) %(objectsize:disk) %(rest) %(deltabase)" && + + # The hardcoded SPs between the atoms are respected. + echo " " >expect && + git cat-file --batch-command="$fmt" >actual <<-EOF && + remote-object-info "$GIT_DAEMON_URL/parent" $hello_oid + EOF + test_cmp expect actual + ) +' + test_expect_success 'requesting only objectname echoes back' ' ( set_transport_variables "$daemon_parent" && @@ -430,10 +448,10 @@ test_expect_success 'batch-command remote-object-info file:// default filter' ' server_path="$(pwd)/server" && cd file_client_empty && - echo "$hello_oid $hello_size" >expect && - echo "$tree_oid $tree_size" >>expect && - echo "$commit_oid $commit_size" >>expect && - echo "$tag_oid $tag_size" >>expect && + echo "$hello_oid $hello_type $hello_size" >expect && + echo "$tree_oid $tree_type $tree_size" >>expect && + echo "$commit_oid $commit_type $commit_size" >>expect && + echo "$tag_oid $tag_type $tag_size" >>expect && git cat-file --batch-command >actual <<-EOF && remote-object-info "file://${server_path}" $hello_oid $tree_oid @@ -449,10 +467,10 @@ test_expect_success 'batch-command -Z remote-object-info file:// default filter' server_path="$(pwd)/server" && cd file_client_empty && - printf "%s\0" "$hello_oid $hello_size" >expect && - printf "%s\0" "$tree_oid $tree_size" >>expect && - printf "%s\0" "$commit_oid $commit_size" >>expect && - printf "%s\0" "$tag_oid $tag_size" >>expect && + printf "%s\0" "$hello_oid $hello_type $hello_size" >expect && + printf "%s\0" "$tree_oid $tree_type $tree_size" >>expect && + printf "%s\0" "$commit_oid $commit_type $commit_size" >>expect && + printf "%s\0" "$tag_oid $tag_type $tag_size" >>expect && printf "%s\0" "$hello_oid missing" >>expect && printf "%s\0" "$tree_oid missing" >>expect && @@ -600,10 +618,10 @@ test_expect_success 'batch-command remote-object-info http:// default filter' ' set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" && cd "$HTTPD_DOCUMENT_ROOT_PATH/http_client_empty" && - echo "$hello_oid $hello_size" >expect && - echo "$tree_oid $tree_size" >>expect && - echo "$commit_oid $commit_size" >>expect && - echo "$tag_oid $tag_size" >>expect && + echo "$hello_oid $hello_type $hello_size" >expect && + echo "$tree_oid $tree_type $tree_size" >>expect && + echo "$commit_oid $commit_type $commit_size" >>expect && + echo "$tag_oid $tag_type $tag_size" >>expect && git cat-file --batch-command >actual <<-EOF && remote-object-info "$HTTPD_URL/smart/http_parent" $hello_oid $tree_oid @@ -618,10 +636,10 @@ test_expect_success 'batch-command -Z remote-object-info http:// default filter' set_transport_variables "$HTTPD_DOCUMENT_ROOT_PATH/http_parent" && cd "$HTTPD_DOCUMENT_ROOT_PATH/http_client_empty" && - printf "%s\0" "$hello_oid $hello_size" >expect && - printf "%s\0" "$tree_oid $tree_size" >>expect && - printf "%s\0" "$commit_oid $commit_size" >>expect && - printf "%s\0" "$tag_oid $tag_size" >>expect && + printf "%s\0" "$hello_oid $hello_type $hello_size" >expect && + printf "%s\0" "$tree_oid $tree_type $tree_size" >>expect && + printf "%s\0" "$commit_oid $commit_type $commit_size" >>expect && + printf "%s\0" "$tag_oid $tag_type $tag_size" >>expect && batch_input="remote-object-info $HTTPD_URL/smart/http_parent $hello_oid $tree_oid remote-object-info $HTTPD_URL/smart/http_parent $commit_oid $tag_oid diff --git a/t/t3200-branch.sh b/t/t3200-branch.sh index b5a56ff25d871c..cdb6c6a6342a0c 100755 --- a/t/t3200-branch.sh +++ b/t/t3200-branch.sh @@ -1757,4 +1757,425 @@ test_expect_success 'errors if given a bad branch name' ' test_cmp expect actual ' +test_expect_success '--forked: setup' ' + test_create_repo forked-upstream && + ( + cd forked-upstream && + test_commit base && + git branch one base && + git branch two base + ) && + + test_create_repo forked-other && + ( + cd forked-other && + test_commit other-base && + git branch foreign other-base + ) && + + git clone forked-upstream forked && + ( + cd forked && + git remote add -f other ../forked-other && + git branch local-base && + git branch --track local-one origin/one && + git branch --track local-two origin/two && + git branch --track local-foreign other/foreign && + git branch --track local-onbase local-base && + + git checkout local-one && + test_commit --no-tag local-one-work local-one.t && + git checkout local-foreign && + test_commit --no-tag local-foreign-work local-foreign.t + ) +' + +test_expect_success '--forked filters by upstream' ' + git -C forked branch --forked origin/one \ + --format="%(refname:short)" >actual && + echo local-one >expect && + test_cmp expect actual +' + +test_expect_success '--forked filters by wildmatch' ' + git -C forked branch --forked "origin/*" \ + --format="%(refname:short)" >actual && + cat >expect <<-\EOF && + local-one + local-two + main + EOF + test_cmp expect actual +' + +test_expect_success '--forked matches branches with local upstream' ' + git -C forked branch --forked local-base \ + --format="%(refname:short)" >actual && + echo local-onbase >expect && + test_cmp expect actual +' + +test_expect_success '--forked can be repeated to widen the filter' ' + git -C forked branch --forked origin/one \ + --forked other/foreign \ + --format="%(refname:short)" >actual && + cat >expect <<-\EOF && + local-foreign + local-one + EOF + test_cmp expect actual +' + +test_expect_success '--forked combines literal and glob arguments' ' + git -C forked branch --forked local-base \ + --forked "other/*" \ + --format="%(refname:short)" >actual && + cat >expect <<-\EOF && + local-foreign + local-onbase + EOF + test_cmp expect actual +' + +test_expect_success '--forked "*/*" covers every remote-tracking upstream' ' + git -C forked branch --forked "*/*" \ + --format="%(refname:short)" >actual && + cat >expect <<-\EOF && + local-foreign + local-one + local-two + main + EOF + test_cmp expect actual +' + +test_expect_success '--forked composes with --no-merged' ' + git -C forked branch --forked "origin/*" \ + --no-merged origin/one \ + --format="%(refname:short)" >actual && + echo local-one >expect && + test_cmp expect actual +' + +test_expect_success '--forked uses the branch /HEAD points at' ' + git -C forked branch --forked origin \ + --format="%(refname:short)" >actual && + echo main >expect && + test_cmp expect actual +' + +test_expect_success '--forked narrows a argument' ' + git -C forked branch --forked "origin/*" "local-*" \ + --format="%(refname:short)" >actual && + cat >expect <<-\EOF && + local-one + local-two + EOF + test_cmp expect actual +' + +test_expect_success '--forked rejects unknown branch/pattern' ' + test_must_fail git -C forked branch --forked nope 2>err && + test_grep "not a valid branch or pattern" err +' + +test_expect_success '--forked requires a value' ' + test_must_fail git -C forked branch --forked 2>err && + test_grep "requires a value" err +' + +test_expect_success '--delete-merged: setup' ' + git init -b main upstream && + ( + cd upstream && + test_commit base && + git checkout -b next && + test_commit next-work && + git checkout main + ) && + git init -b main other && + test_commit -C other other-base && + git init -b main fork +' + +setup_repo_for_delete_merged () { + rm -rf repo && + git clone upstream repo && + ( + cd repo && + git remote add fork ../fork && + git remote add other ../other && + git config push.default current && + git fetch other + ) +} + +create_merged_branch () { + ( + cd repo && + git checkout -b "$1" --track origin/next && + git commit --allow-empty -m "$1 work" && + git push origin "$1:next" + ) +} + +check_branches () { + git for-each-ref --format="%(refname:short)" refs/heads/ >actual && + cat >expect && + test_cmp expect actual +} + +test_expect_success '--delete-merged keeps cloned main without explicit push configuration' ' + setup_repo_for_delete_merged && + ( + cd repo && + test_cmp_config origin branch.main.remote && + test_cmp_config refs/heads/main branch.main.merge && + git checkout --detach && + + git branch --delete-merged */* && + + check_branches <<-\EOF + main + EOF + ) +' + +test_expect_success '--delete-merged deletes only selected merged branches' ' + setup_repo_for_delete_merged && + create_merged_branch also-merged && + create_merged_branch merged && + ( + cd repo && + git checkout -b unmerged --track origin/next && + git commit --allow-empty -m "unmerged work" && + git checkout -b tracks-other --track other/main && + sha=$(git rev-parse --short merged) && + + git branch --dry-run --delete-merged origin/next merged \ + >actual 2>&1 && + echo "Would delete branch merged (was $sha)." >expect && + test_cmp expect actual && + git rev-parse --verify refs/heads/merged && + + check_branches <<-\EOF && + also-merged + main + merged + tracks-other + unmerged + EOF + + git branch --delete-merged origin/next merged >actual 2>&1 && + echo "Deleted branch merged (was $sha)." >expect && + test_cmp expect actual && + + check_branches <<-\EOF + also-merged + main + tracks-other + unmerged + EOF + ) +' + +test_expect_success '--delete-merged keeps main despite a different default push remote' ' + setup_repo_for_delete_merged && + create_merged_branch on-next && + create_merged_branch checked-out && + create_merged_branch upstream-gone && + ( + cd repo && + git config remote.pushDefault fork && + git checkout -b local-to-delete --track main && + git config branch.upstream-gone.merge refs/heads/topic && + git checkout -b tracks-other --track other/main && + git checkout checked-out && + + git branch --delete-merged origin/* --delete-merged main && + + check_branches <<-\EOF + checked-out + main + tracks-other + upstream-gone + EOF + ) +' + +test_expect_success '--delete-merged maps push refspecs to upstreams' ' + setup_repo_for_delete_merged && + ( + cd repo && + git checkout -b topic && + git config remote.origin.push \ + "refs/heads/topic:refs/heads/published" && + git push origin && + git branch --set-upstream-to=origin/published topic && + git checkout -b other-topic --track origin/published && + git checkout --detach && + + git branch --delete-merged origin/published && + + check_branches <<-\EOF + main + topic + EOF + ) +' + +test_expect_success '--delete-merged keeps the upstream of a surviving branch' ' + setup_repo_for_delete_merged && + create_merged_branch feature && + ( + cd repo && + git checkout -b topic --track feature && + git commit --allow-empty -m "topic work" && + + git branch --dry-run --delete-merged origin/next >out && + test_grep ! "feature" out && + + git branch --delete-merged origin/next 2>err && + test_must_be_empty err && + + check_branches <<-\EOF && + feature + main + topic + EOF + + pattern="branch\\.(feature|topic)\\.(merge|remote)" && + git config --local --get-regexp "$pattern" >actual && + cat >expect <<-\EOF && + branch.feature.remote origin + branch.feature.merge refs/heads/next + branch.topic.remote . + branch.topic.merge refs/heads/feature + EOF + test_cmp expect actual + ) +' + +test_expect_success '--delete-merged clears the deleted upstream of a protected branch' ' + setup_repo_for_delete_merged && + ( + cd repo && + git branch --track lower origin/next && + git branch --track mid lower && + git checkout -b tip --track mid && + git commit --allow-empty -m "tip work" && + sha=$(git rev-parse --short lower) && + + git branch --dry-run --delete-merged origin/next \ + --delete-merged lower >actual 2>&1 && + echo "Would delete branch lower (was $sha)." >expect && + test_cmp expect actual && + + pattern="branch\\.(lower|mid|tip)\\.(merge|remote)" && + git config --local --get-regexp "$pattern" >actual && + cat >expect <<-\EOF && + branch.lower.remote origin + branch.lower.merge refs/heads/next + branch.mid.remote . + branch.mid.merge refs/heads/lower + branch.tip.remote . + branch.tip.merge refs/heads/mid + EOF + test_cmp expect actual && + + git branch --delete-merged origin/next \ + --delete-merged lower >actual 2>&1 && + echo "Deleted branch lower (was $sha)." >expect && + test_cmp expect actual && + + check_branches <<-\EOF && + main + mid + tip + EOF + + pattern="branch\\.(mid|tip)\\.(merge|remote)" && + git config --local --get-regexp "$pattern" >actual && + cat >expect <<-\EOF && + branch.tip.remote . + branch.tip.merge refs/heads/mid + EOF + test_cmp expect actual + ) +' + +test_expect_success '--delete-merged result is independent of stacked branch names' ' + setup_repo_for_delete_merged && + ( + cd repo && + git branch --track c-lower origin/next && + git branch --track b-mid c-lower && + git checkout -b a-tip --track b-mid && + git commit --allow-empty -m "tip work" && + + git branch --delete-merged origin/next --delete-merged "c-*" && + + check_branches <<-\EOF && + a-tip + b-mid + main + EOF + + git branch --delete-merged origin/next \ + --delete-merged "c-*" >actual 2>&1 && + test_must_be_empty actual && + + check_branches <<-\EOF + a-tip + b-mid + main + EOF + ) +' + +test_expect_success '--delete-merged requires a value' ' + test_must_fail git -C forked branch --delete-merged 2>err && + test_grep "requires a value" err +' + +test_expect_success '--delete-merged honours branch..deleteMerged=false' ' + setup_repo_for_delete_merged && + create_merged_branch deleted && + create_merged_branch kept && + ( + cd repo && + git config branch.kept.deleteMerged false && + git checkout --detach && + + git branch --delete-merged origin/next 2>err && + + test_grep "Skipping .kept." err && + check_branches <<-\EOF + kept + main + EOF + ) +' + +test_expect_success "branch -d still deletes a deleteMerged=false branch" ' + setup_repo_for_delete_merged && + create_merged_branch kept && + ( + cd repo && + git config branch.kept.deleteMerged false && + git checkout --detach && + + git branch -d kept && + + check_branches <<-\EOF + main + EOF + ) +' + +test_expect_success '--dry-run without --delete-merged is rejected' ' + test_must_fail git -C forked branch --dry-run 2>err && + test_grep "requires --delete-merged" err +' + test_done diff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh index 7a3127e8a9b960..ff11abb2f222c4 100755 --- a/t/t3404-rebase-interactive.sh +++ b/t/t3404-rebase-interactive.sh @@ -65,6 +65,24 @@ test_expect_success 'setup' ' test_commit P fileP ' +test_expect_success MINGW 'rebase releases object database before committing' ' + test_when_finished "rm -f .git/hooks/post-commit repacked packs" && + git switch -C repack-rewrite primary && + git repack -ad && + write_script .git/hooks/post-commit <<-\EOF && + git repack -ad && + >repacked + EOF + ( + set_fake_editor && + FAKE_LINES="reword 1" GIT_TEST_LEGACY_DELETE=1 \ + git -c core.commitGraph=false rebase -i HEAD^ + ) && + test_path_is_file repacked && + ls .git/objects/pack/*.pack >packs && + test_line_count = 1 packs +' + # "exec" commands are run with the user shell by default, but this may # be non-POSIX. For example, if SHELL=zsh then ">file" doesn't work # to create a file. Unsetting SHELL avoids such non-portable behavior diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index 3353bc4a4dc6ed..255bae58460297 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -52,8 +52,19 @@ test_expect_success 'setup' ' test_merge P O --no-ff && git switch main && + git switch --orphan unrelated && + test_commit unrelated-root && + git switch -c conflict B && - test_commit C.conflict C.t conflict + test_commit C.conflict C.t conflict && + git branch -D unrelated && + + git switch -c divergent-x main && + test_commit X && + git switch -c divergent-y main && + test_commit Y && + git switch divergent-x && + test_merge Z divergent-y --no-ff ' test_expect_success 'setup bare' ' @@ -565,4 +576,100 @@ test_expect_success '--onto with --ref rejects multiple revision ranges' ' test_grep "cannot be used with multiple revision ranges" err ' +test_expect_success 'replay to rebase merge commit with --linearize' ' + git replay --ref-action=print --linearize \ + --onto main I..topic-with-merge >result && + + test_line_count = 1 result && + + git log --format=%s $(cut -f 3 -d " " result) >actual && + test_write_lines O N J M L B A >expect && + test_cmp expect actual +' + +test_expect_success 'replay to rebase merge commit with --linearize down to the root commit' ' + git replay --ref-action=print --linearize \ + --onto unrelated-root topic-with-merge >result && + + test_line_count = 1 result && + + git log --format=%s $(cut -f 3 -d " " result) >actual && + test_write_lines O N J I B A unrelated-root >expect && + test_cmp expect actual +' + +test_expect_success 'replay to cherry-pick merge commit with --linearize' ' + git replay --ref-action=print --linearize \ + --advance main I..topic-with-merge >result && + + test_line_count = 1 result && + + git log --format=%s $(cut -f 3 -d " " result) >actual && + test_write_lines O N J M L B A >expect && + test_cmp expect actual && + + printf "update refs/heads/main " >expect && + printf "%s " $(cut -f 3 -d " " result) >>expect && + git rev-parse main >>expect && + test_cmp expect result +' + +test_expect_success 'replay --linearize produces the same patches' ' + git replay --ref-action=print --linearize \ + --onto main I..topic-with-merge >result && + + test_line_count = 1 result && + tip=$(cut -f 3 -d " " result) && + + # range-diff does not care about the dropped merge, + # so the original commits (I..topic-with-merge) + # and the replayed chain (main..tip) must produce identical patches. + git range-diff I..topic-with-merge main..$tip >out && + test_file_not_empty out && + test_grep ! -v "=" out && + + git log --oneline main..$tip >out && + test_line_count = 3 out +' + +test_expect_success '--linearize rejects multiple revision ranges' ' + test_must_fail git replay --ref-action=print --linearize \ + --onto main ^B topic2 topic3 topic4 2>err && + test_grep "cannot be used with multiple revision ranges" err +' + +test_expect_success 'replay with --linearize of a divergent merge keeps both sides' ' + git replay --ref-action=print --linearize \ + --onto main main..divergent-x >result && + test_line_count = 1 result && + tip=$(cut -f 3 -d " " result) && + + # The merge Z is dropped, but both X and Y are linearized onto main; + # neither side is lost. + git log --format=%s main..$tip >actual && + test_write_lines Y X >expect && + test_cmp expect actual +' + +test_expect_success '--linearize and --contained cannot be used together' ' + test_must_fail git replay --ref-action=print --linearize --contained \ + --onto main ^B topic-with-merge 2>err && + test_grep "cannot be used together" err +' + +test_expect_success 'replay --revert with --linearize reverts a range containing a merge' ' + git replay --ref-action=print --revert=divergent-x --linearize \ + main..divergent-x >result && + test_line_count = 1 result && + tip=$(cut -f 3 -d " " result) && + + git log --format=%s $tip >actual && + test_write_lines \ + "Revert \"X\"" "Revert \"Y\"" Z Y X M L B A >expect && + test_cmp expect actual && + + test_must_fail git cat-file -e $tip:X.t && + test_must_fail git cat-file -e $tip:Y.t +' + test_done diff --git a/t/t5701-git-serve.sh b/t/t5701-git-serve.sh index 9a575aa098afd3..30f6c41af00397 100755 --- a/t/t5701-git-serve.sh +++ b/t/t5701-git-serve.sh @@ -71,6 +71,17 @@ test_expect_success 'request invalid capability' ' test_grep "unknown capability" err ' +test_expect_success 'promisor-remote capability requires an argument' ' + test-tool pkt-line pack >in <<-EOF && + command=ls-refs + object-format=$(test_oid algo) + promisor-remote + 0000 + EOF + test_must_fail test-tool serve-v2 --stateless-rpc 2>err in <<-EOF && agent=git/test @@ -344,20 +355,53 @@ test_expect_success 'unexpected lines are not allowed in fetch request' ' test_expect_success 'basics of object-info' ' test_config transfer.advertiseObjectInfo true && + two_oid=$(git rev-parse two:two.t) && + two_size=$(test_file_size two.t) && + test-tool pkt-line pack >in <<-EOF && command=object-info object-format=$(test_oid algo) 0001 size - oid $(git rev-parse two:two.t) - oid $(git rev-parse two:two.t) + oid $two_oid + oid $two_oid + 0000 + EOF + + cat >expect <<-EOF && + size + $two_oid $two_size + $two_oid $two_size + 0000 + EOF + + test-tool serve-v2 --stateless-rpc out && + test-tool pkt-line unpack actual && + test_cmp expect actual +' + +test_expect_success 'object-info supports type' ' + test_config transfer.advertiseObjectInfo true && + + two_oid=$(git rev-parse two:two.t) && + two_size=$(test_file_size two.t) && + + test-tool pkt-line pack >in <<-EOF && + command=object-info + object-format=$(test_oid algo) + 0001 + size + type + oid $two_oid + oid $two_oid 0000 EOF cat >expect <<-EOF && size - $(git rev-parse two:two.t) $(wc -c /dev/null + EOF + git bisect start $HASH4 $HASH2 && + git bisect run "$1" ./test_script.sh >my_bisect_log.txt && + test_grep "$HASH3 is the first .bad. commit" my_bisect_log.txt && + test_bisect_state_missing BISECT_RUN +} + +test_reset_when_found_fails () { + local pattern="$1" && + local state_file="$2" && + shift 2 && + test_must_fail "$@" 2>err && + test_grep -- "$pattern" err && + test_bisect_state_missing "$state_file" +} + test_expect_success 'bisect usage' " test_bisect_usage 1 git bisect reset extra1 extra2 <<-\EOF && error: 'git bisect reset' requires either no argument or a commit @@ -453,6 +489,91 @@ test_expect_success '"git bisect run" simple case' ' git bisect reset ' +test_expect_success '"git bisect start --reset-when-found" defaults to original' ' + test_when_finished "git bisect reset && git checkout main" && + git checkout main && + bisect_start_and_finish --reset-when-found && + actual=$(git rev-parse HEAD) && + test "$HASH4" = "$actual" && + actual=$(git branch --show-current) && + test main = "$actual" && + test_bisect_state_missing BISECT_START && + + bisect_start_and_finish --reset-when-found=original && + actual=$(git rev-parse HEAD) && + test "$HASH4" = "$actual" && + actual=$(git branch --show-current) && + test main = "$actual" && + test_bisect_state_missing BISECT_START +' + +test_expect_success '"git bisect start --reset-when-found=found" leaves first bad checked out' ' + test_when_finished "git bisect reset && git checkout main" && + bisect_start_and_finish --reset-when-found=found && + actual=$(git rev-parse HEAD) && + test "$HASH3" = "$actual" && + test_bisect_state_missing BISECT_START +' + +test_expect_success '"git bisect run --reset-when-found" defaults to original' ' + test_when_finished "git bisect reset && git checkout main" && + bisect_run_reset_when_found --reset-when-found && + actual=$(git rev-parse HEAD) && + test "$HASH4" = "$actual" && + actual=$(git branch --show-current) && + test main = "$actual" && + test_bisect_state_missing BISECT_START +' + +test_expect_success '"git bisect run --reset-when-found=found" leaves first bad checked out' ' + test_when_finished "git bisect reset && git checkout main" && + bisect_run_reset_when_found --reset-when-found=found && + actual=$(git rev-parse HEAD) && + test "$HASH3" = "$actual" && + test_bisect_state_missing BISECT_START +' + +test_expect_success '--reset-when-found rejects an unknown reset target' ' + test_when_finished "git bisect reset && git checkout main" && + test_reset_when_found_fails \ + "invalid value for.*--reset-when-found.*unknown" BISECT_START \ + git bisect start --reset-when-found=unknown $HASH4 $HASH2 && + + git bisect start $HASH4 $HASH2 && + test_reset_when_found_fails \ + "invalid value for.*--reset-when-found.*unknown" \ + BISECT_RESET_WHEN_FOUND \ + git bisect run --reset-when-found=unknown true +' + +test_expect_success '--reset-when-found cannot be used with --no-checkout' ' + test_when_finished "git bisect reset" && + test_reset_when_found_fails \ + "options .*--reset-when-found.* and .*--no-checkout.* cannot be used together" BISECT_START \ + git bisect start --reset-when-found=original --no-checkout $HASH4 $HASH2 && + + git bisect start --no-checkout $HASH4 $HASH2 && + test_reset_when_found_fails \ + "options .*--reset-when-found.* and .*--no-checkout.* cannot be used together" BISECT_RESET_WHEN_FOUND \ + git bisect run --reset-when-found=found true +' + +test_expect_success 'without --reset-when-found the bisection state is kept' ' + test_when_finished "git bisect reset" && + git bisect start $HASH4 $HASH2 && + git bisect bad && + test_bisect_state_file BISECT_START +' + +test_expect_success '--reset-when-found does not leak into a later bisection' ' + test_when_finished "git bisect reset && git checkout main" && + bisect_start_and_finish --reset-when-found && + + git bisect start $HASH4 $HASH2 && + git bisect bad && + test_bisect_state_file BISECT_START +' + # We want to automatically find the commit that # added "Ciao" into hello. test_expect_success '"git bisect run" with more complex "git bisect start"' ' diff --git a/t/t6099-merge-base-side-exhaustion.sh b/t/t6099-merge-base-side-exhaustion.sh new file mode 100755 index 00000000000000..0e12b5a0b06366 --- /dev/null +++ b/t/t6099-merge-base-side-exhaustion.sh @@ -0,0 +1,82 @@ +#!/bin/sh + +test_description='merge-base with ancestor among merge-base candidates + +Test that merge-base --all correctly handles cases where +multiple merge-base candidates exist and one is an ancestor +of another. The side-exhaustion optimization in +paint_down_to_common may exit before STALE propagation +removes the ancestor, but remove_redundant catches it. + +Graph shape (parents are below children): + + A ----- X + |\ /| + | B---/ | + | \ | + e2 \ f2 + | | | + e1 d1 f1 + \ | / + \ | / + \|/ + C + +A and X are the two tips. +B and C are both reachable from A and X. +B reaches C through d1. +Only B should appear in merge-base --all output. +' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +TEST_PASSES_SANITIZE_LEAK=true +. ./test-lib.sh + +test_expect_success 'setup ancestor merge-base candidate' ' + test_commit C && + + git checkout -b d-chain HEAD && + test_commit d1 && + test_commit B && + + git checkout -b e-path C && + test_commit e1 && + test_commit e2 && + + git checkout -b f-path C && + test_commit f1 && + test_commit f2 && + + git checkout -b branch-A e-path && + test_merge A B && + + git checkout -b branch-X f-path && + test_merge X B && + + git commit-graph write --reachable +' + +test_expect_success 'merge-base --all excludes ancestor candidate' ' + git rev-parse B >expected && + git merge-base --all A X >actual && + test_cmp expected actual +' + +test_expect_success 'merge-base (single) finds shallowest' ' + git rev-parse B >expected && + git merge-base A X >actual && + test_cmp expected actual +' + +# Without commit-graph: generation numbers are INFINITY, +# side-exhaustion optimization does not fire. +test_expect_success 'merge-base --all without commit-graph' ' + rm -f .git/objects/info/commit-graph && + git rev-parse B >expected && + git merge-base --all A X >actual && + test_cmp expected actual +' + +test_done diff --git a/t/t6600-test-reach.sh b/t/t6600-test-reach.sh index 019fed9f5ec0ea..13c84e0a423138 100755 --- a/t/t6600-test-reach.sh +++ b/t/t6600-test-reach.sh @@ -85,6 +85,103 @@ test_expect_success 'setup' ' git branch -f skew-P2 "$skew_P2" && git tag skew-M2 "$skew_M2" && + # Build a small side topology to exercise the (PARENT1|PARENT2) -> + # (PARENT1|PARENT2|STALE) transition in paint_down_to_common(); the + # 10x10 grid above does not exercise it because no merge-base candidate + # there is a descendant of another, so STALE never reaches a + # still-pending candidate. + # + # ps-X + # /|\ + # / | \ + # ps-Z ps-B ps-W + # | / \ | + # | / \ | + # |/ \| + # ps-T1 ps-T2 + # + # where ps-T1=merge(ps-Z,ps-B), ps-T2=merge(ps-W,ps-B), so + # merge-base(ps-T1,ps-T2) = ps-B. During the walk, ps-X transitions + # to (PARENT1|PARENT2) via ps-Z and ps-W before ps-B is dequeued; + # then the STALE-walk from ps-B transitions ps-X to + # (PARENT1|PARENT2|STALE). + git checkout --orphan ps-orphan && + test_commit ps-X && + git checkout -b ps-B-br ps-X && test_commit ps-B && + git checkout -b ps-Z-br ps-X && test_commit ps-Z && + git checkout -b ps-W-br ps-X && test_commit ps-W && + git checkout -b ps-T1 ps-Z && + git merge --no-ff -m ps-T1 ps-B && + git checkout -b ps-T2 ps-W && + git merge --no-ff -m ps-T2 ps-B && + + # Build a side topology that lives entirely outside the half + # commit-graph and has non-monotonic commit dates, to exercise the + # INFINITY-gate in paint_down_to_common. With both tips outside + # the graph, generation is INFINITY and the queue falls back to + # commit-date order, which here is non-monotonic. + # + # pi-X (date 500, PARENT1 tip) --> pi-P, pi-D + # pi-D (date 480) --> pi-C + # pi-C (date 200) --> pi-B + # pi-B (date 100, PARENT2 tip) --> pi-P + # pi-P (date 450, root) + # + # merge-base(pi-X, pi-B) = pi-B (it is an ancestor of pi-X and is + # itself one of the queried tips). + git checkout --orphan pi-orphan && + test_commit --date "@450 +0000" pi-P && + test_commit --date "@100 +0000" pi-B && + test_commit --date "@200 +0000" pi-C && + test_commit --date "@480 +0000" pi-D && + GIT_AUTHOR_DATE="@500 +0000" GIT_COMMITTER_DATE="@500 +0000" \ + git commit-tree -p pi-D -p pi-P -m pi-X pi-D^{tree} >pi-X-oid && + pi_x="$(cat pi-X-oid)" && + git branch -f pi-X-br "$pi_x" && + git tag pi-X "$pi_x" && + + # Clock-skew topology for side-exhaustion testing. + # D is the correct merge base but has a higher committer date + # than C (its child). With date ordering, D would be dequeued + # before C, causing side-exhaustion to fire too early. + # Generation ordering prevents this by visiting children + # before parents regardless of dates. + # + # se-A (date 7000) --> se-C (date 3000) --> se-D (date 5000) --> se-root (date 4000) + # se-B (date 6000) --> se-D + # + se_root=$(skew_commit 4000 se-root) && + se_D=$(skew_commit 5000 se-D -p "$se_root") && + se_C=$(skew_commit 3000 se-C -p "$se_D") && + se_A=$(skew_commit 7000 se-A -p "$se_C") && + se_B=$(skew_commit 6000 se-B -p "$se_D") && + git branch -f se-A "$se_A" && + git branch -f se-B "$se_B" && + git tag se-D "$se_D" && + + # Clock-skew topology with redundant ancestor for + # side-exhaustion testing. MB1 is the correct merge base; + # MB2 is its parent. A reaches MB2 via E (high date) and + # MB1 via C (low date). B reaches MB1 via D. With date + # ordering, side-exhaustion would fire before C is dequeued, + # missing MB1. Generation ordering ensures both are found. + # + # se2-A (date 8000) --> se2-C (date 2000) --> se2-MB1 (date 5000) --> se2-MB2 (date 4000) --> se2-root (date 1000) + # se2-A --> se2-E (date 6500) --> se2-MB2 + # se2-B (date 7000) --> se2-D (date 6000) --> se2-MB1 + # + se2_root=$(skew_commit 1000 se2-root) && + se2_MB2=$(skew_commit 4000 se2-MB2 -p "$se2_root") && + se2_MB1=$(skew_commit 5000 se2-MB1 -p "$se2_MB2") && + se2_C=$(skew_commit 2000 se2-C -p "$se2_MB1") && + se2_D=$(skew_commit 6000 se2-D -p "$se2_MB1") && + se2_E=$(skew_commit 6500 se2-E -p "$se2_MB2") && + se2_A=$(skew_commit 8000 se2-A -p "$se2_C" -p "$se2_E") && + se2_B=$(skew_commit 7000 se2-B -p "$se2_D") && + git branch -f se2-A "$se2_A" && + git branch -f se2-B "$se2_B" && + git tag se2-MB1 "$se2_MB1" && + git commit-graph write --reachable && mv .git/objects/info/commit-graph commit-graph-full && chmod u+w commit-graph-full && @@ -98,24 +195,34 @@ test_expect_success 'setup' ' ' run_all_modes () { - test_when_finished rm -rf .git/objects/info/commit-graph && - "$@" actual && - test_cmp expect actual && - cp commit-graph-full .git/objects/info/commit-graph && - "$@" actual && - test_cmp expect actual && - cp commit-graph-half .git/objects/info/commit-graph && - "$@" actual && - test_cmp expect actual && - cp commit-graph-no-gdat .git/objects/info/commit-graph && - "$@" actual && - test_cmp expect actual + graph=.git/objects/info/commit-graph && + test_when_finished rm -rf "$graph" "${graph}s" && + rm -f trace-mode-*.txt && + + for mode in none full half no-gdat + do + rm -rf "$graph" "${graph}s" && + cp "commit-graph-${mode}" "$graph" 2>/dev/null || + true && + GIT_TRACE2_EVENT="$(pwd)/trace-mode-${mode}.txt" \ + "$@" actual && + test_cmp expect actual || return 1 + done } test_all_modes () { run_all_modes test-tool reach "$@" } +test_paint_down_steps () { + for mode in none full half no-gdat + do + test_trace2_data_singular paint_down_to_common steps "$1" \ + "mode=$mode" <"trace-mode-${mode}.txt" || return 1 + shift + done +} + test_expect_success 'ref_newer:miss' ' cat >input <<-\EOF && A:commit-5-7 @@ -182,6 +289,17 @@ test_expect_success 'in_merge_bases_many:miss-heuristic' ' test_all_modes in_merge_bases_many ' +test_expect_success 'in_merge_bases_many:self' ' + cat >input <<-\EOF && + A:commit-6-8 + X:commit-5-9 + X:commit-6-8 + EOF + echo "in_merge_bases_many(A,X):1" >expect && + test_all_modes in_merge_bases_many && + test_paint_down_steps 45 1 25 1 +' + test_expect_success 'is_descendant_of:hit' ' cat >input <<-\EOF && A:commit-5-7 @@ -219,6 +337,105 @@ test_expect_success 'get_merge_bases_many' ' test_all_modes get_merge_bases_many ' +test_expect_success 'get_merge_bases_many:duplicate-twos' ' + cat >input <<-\EOF && + A:commit-5-7 + X:commit-4-8 + X:commit-4-8 + X:commit-6-6 + X:commit-6-6 + X:commit-8-3 + EOF + { + echo "get_merge_bases_many(A,X):" && + git rev-parse commit-5-6 \ + commit-4-7 | sort + } >expect && + test_all_modes get_merge_bases_many +' + +test_expect_success 'get_merge_bases_many:pending-stale' ' + # Exercises the (PARENT1|PARENT2) -> (...|STALE) transition path in + # paint_down_to_common(). See the topology comment in the setup test. + cat >input <<-\EOF && + A:ps-T1 + X:ps-T2 + EOF + { + echo "get_merge_bases_many(A,X):" && + git rev-parse ps-B + } >expect && + test_all_modes get_merge_bases_many && + test_paint_down_steps 5 5 5 5 +' + +test_expect_success 'get_merge_bases_many:infinity-both-sides' ' + # Exercises the push-time INFINITY-gate in paint_down_to_common(). See + # the pi-* topology comment in the setup test. + cat >input <<-\EOF && + A:pi-X + X:pi-B + EOF + { + echo "get_merge_bases_many(A,X):" && + git rev-parse pi-B + } >expect && + test_all_modes get_merge_bases_many && + test_paint_down_steps 5 4 5 4 +' + +test_expect_success 'setup mixed finite/INFINITY topology' ' + # Create a commit outside all saved commit-graph files so it always + # has INFINITY generation, while its parent (ps-X) is in the graph + # with a finite generation. Use the ps-* orphan topology so we do + # not pollute the grid-based rev-list tests. + git checkout ps-X && + test_env GIT_TEST_COMMIT_GRAPH= test_commit pm-INF +' + +test_expect_success 'get_merge_bases_many:mixed-finite-infinity' ' + # One tip (pm-INF) is outside the commit-graph with INFINITY + # generation; the other (ps-B) is in the graph with finite + # generation. The walk starts in the INFINITY region and crosses + # into the finite region where side-exhaustion can fire. + cat >input <<-\EOF && + A:pm-INF + X:ps-B + EOF + { + echo "get_merge_bases_many(A,X):" && + git rev-parse ps-X + } >expect && + test_all_modes get_merge_bases_many && + test_paint_down_steps 3 3 3 3 +' + +test_expect_success 'merge-base --all commit-walk steps' ' + >input && + git rev-parse commit-9-1 >expect && + run_all_modes git merge-base --all commit-9-9 commit-9-1 && + test_paint_down_steps 81 9 57 37 +' + +test_expect_success 'merge-base --all with clock skew (side-exhaustion)' ' + # Verify that the merge base is computed correctly even + # when commits have non-monotonic commit dates. + >input && + git rev-parse se-D >expect && + run_all_modes git merge-base --all se-A se-B && + test_paint_down_steps 6 4 6 4 +' + +test_expect_success 'merge-base --all with clock skew and redundant ancestor (side-exhaustion)' ' + # Verify that the correct merge base is found even when + # non-monotonic commit dates could cause a redundant + # ancestor to be visited first. + >input && + git rev-parse se2-MB1 >expect && + run_all_modes git merge-base --all se2-A se2-B && + test_paint_down_steps 8 6 8 6 +' + test_expect_success 'reduce_heads' ' cat >input <<-\EOF && X:commit-1-10 diff --git a/t/t7201-co.sh b/t/t7201-co.sh index 0ddd1ad7aab06a..f9696dab36618b 100755 --- a/t/t7201-co.sh +++ b/t/t7201-co.sh @@ -240,6 +240,14 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' ' test_grep "git stash drop" actual && test_grep "git stash pop" actual && test_grep "The following paths have local changes" actual && + sed -n "/apply the local changes later/,/Switched to branch/p" \ + actual >separator.actual && + cat >separator.expect <<-EOF && + apply the local changes later by running "git stash pop". + + Switched to branch ${SQ}side${SQ} + EOF + test_cmp separator.expect separator.actual && git log -p -1 --format="%gs%n%B" -g --diff-merges=1 refs/stash >actual && sed /^index/d actual >actual.trimmed && cat >expect <<-EOF && @@ -262,11 +270,18 @@ test_expect_success 'checkout -m creates a recoverable stash on conflict' ' git reset --hard ' -test_expect_success 'checkout -m which would overwrite untracked file' ' +test_expect_success 'checkout -m only retries untracked-file failure with local changes' ' git checkout -f --detach main && test_commit another-file && git checkout HEAD^ && >another-file.t && + test_must_fail env GIT_TRACE2_EVENT="$(pwd)/trace" \ + git checkout -m @{-1} 2>err && + test_grep "untracked working tree files" err && + grep "\"region_enter\".*\"category\":\"index\",\"label\":\"refresh\"" \ + trace >refresh.events && + test_line_count = 1 refresh.events && + fill 1 2 3 4 5 >one && test_must_fail git checkout -m @{-1} 2>err && q_to_tab >expect <<-\EOF && diff --git a/t/t7513-interpret-trailers.sh b/t/t7513-interpret-trailers.sh index 818a8dafbd29e5..e3555b6d51d134 100755 --- a/t/t7513-interpret-trailers.sh +++ b/t/t7513-interpret-trailers.sh @@ -1989,4 +1989,23 @@ test_expect_success 'handling of --- lines in conjunction with cut-lines' ' test_cmp expected actual ' +test_expect_success 'URLs and lines that are not quite URLs' ' + cat >expect <<-\EOF && + https: //www.a-trailer.org + https: //www.another-trailer.org + Signed-off-by: somebody + EOF + git interpret-trailers --only-trailers >actual <<-\EOF && + subject + + body + + https://www.not-a-trailer.org + https ://www.a-trailer.org + https: //www.another-trailer.org + Signed-off-by: somebody + EOF + test_cmp expect actual +' + test_done diff --git a/t/t7706-repack-drop-filtered.sh b/t/t7706-repack-drop-filtered.sh new file mode 100755 index 00000000000000..cb361158349c13 --- /dev/null +++ b/t/t7706-repack-drop-filtered.sh @@ -0,0 +1,185 @@ +#!/bin/sh + +test_description='git repack --drop-filtered enumerates filtered promisor blobs' + +. ./test-lib.sh + +# Delete a loose or packed object from "repo". +delete_object () { + local repo="$1" && + local obj="$2" && + local path="$repo/.git/objects/$(test_oid_to_path "$obj")" && + rm "$path" +} + +# Pack the objects into a promisor pack inside "repo". It is a pack +# accompanied by an empty ".promisor" marker file. Objects +# in such a pack are treated as recoverable from the promisor remote. +pack_as_from_promisor () { + HASH=$(git -C repo pack-objects .git/objects/pack/pack) && + >repo/.git/objects/pack/pack-$HASH.promisor && + echo $HASH +} + +# Write a blob of $1 bytes into "repo", record it as coming from the +# promisor remote, and remove the loose copy so the object is only +# present in the promisor pack. +promisor_blob () { + test-tool genrandom "$1" "$2" >blob_content && + OID=$(git -C repo hash-object -w --stdin /dev/null && + delete_object repo "$OID" && + echo "$OID" +} + +# Check option validation before any promisor walk +test_expect_success 'setup plain repo for validation' ' + git init plain && + test_commit -C plain initial && + git clone --bare plain plain.git && + git -C plain.git repack -a -d +' + +test_expect_success '--drop-filtered requires --filter' ' + test_must_fail git -C plain.git repack --drop-filtered --dry-run -a 2>err && + test_grep "drop-filtered requires --filter" err +' + +test_expect_success '--drop-filtered cannot be used with --filter-to' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --filter-to=./filter-out 2>err && + test_grep "options .--drop-filtered. and .--filter-to. cannot be used together" err +' + +test_expect_success '--dry-run only takes effect with --drop-filtered' ' + test_must_fail git -C plain.git repack --dry-run 2>err && + test_grep "dry-run only takes effect with --drop-filtered" err +' + +test_expect_success '--drop-filtered requires -a' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run 2>err && + test_grep "drop-filtered requires -a" err +' + +test_expect_success '--drop-filtered fails with --write-bitmap-index' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run -a -b 2>err && + test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err +' + +test_expect_success '--drop-filtered rejects explicit -b even when repack.writeBitmaps=true' ' + test_must_fail git -C plain.git -c repack.writeBitmaps=true \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a -b 2>err && + test_grep "options .--drop-filtered. and .--write-bitmap-index. cannot be used together" err +' + +test_expect_success '--drop-filtered fails without a promisor remote' ' + test_must_fail git -C plain.git repack --drop-filtered \ + --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "drop-filtered requires a promisor remote" err +' + +# Enumeration tests using promisor pack +test_expect_success 'setup repo with a promisor remote' ' + rm -rf repo && + test_create_repo repo && + test_commit -C repo base && + + # Mark the repo as a partial clone with a promisor remote so the + # promisor walk and the safety guard are satisfied. + git -C repo config core.repositoryformatversion 1 && + git -C repo config extensions.partialclone origin && + git -C repo config remote.origin.promisor true && + git -C repo config remote.origin.url "." && + + BIG=$(promisor_blob big 3072) && + SMALL=$(promisor_blob small 512) && + echo "$BIG" >big_oid && + echo "$SMALL" >small_oid +' + +test_expect_success 'promisor blob over the threshold is listed' ' + BIG=$(cat big_oid) && + SMALL=$(cat small_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out && + + test_grep "$BIG" out && + test_grep ! "$SMALL" out +' + +test_expect_success 'locally created blob is never listed' ' + BIG=$(cat big_oid) && + + # Large blob that exists only locally must never be a drop candidate. + # Dropping it would be unrecoverable. + test-tool genrandom local 4096 >local_content && + LOCAL=$(git -C repo hash-object -w --stdin out && + + test_grep "$BIG" out && + test_grep ! "$LOCAL" out +' + +test_expect_success '--dry-run does not remove the filtered objects' ' + BIG=$(cat big_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a >out && + + # Candidate blob must still be present after a dry run. + git -C repo cat-file -e "$BIG" +' + +test_expect_success '--drop-filtered removes the promisor blob locally' ' + BIG=$(cat big_oid) && + SMALL=$(cat small_oid) && + + git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k -a && + + git -C repo cat-file --batch-all-objects --batch-check="%(objectname)" >present && + test_grep ! "$BIG" present && + test_grep "$SMALL" present +' + +test_expect_success '--drop-filtered refuses when a merge is in progress' ' + test_when_finished "git -C repo merge --abort || :" && + + # Create a conflicting merge so wt_status reports it. + git -C repo checkout -B mergebase base && + echo one >repo/conflict.txt && + git -C repo add conflict.txt && + git -C repo commit -m one && + + git -C repo checkout -B mergeother base && + echo two >repo/conflict.txt && + git -C repo add conflict.txt && + git -C repo commit -m two && + + test_must_fail git -C repo merge mergebase && + + test_must_fail git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "in progress" err +' + +test_expect_success '--drop-filtered refuses to drop an index-referenced blob' ' + # Create a large blob, add it to the index and make it a promisor object + # so the index references it and enumeration picks it up. + test-tool genrandom idx 4096 >repo/tracked-big.bin && + git -C repo add tracked-big.bin && + OID=$(git -C repo rev-parse :tracked-big.bin) && + printf "%s\n" "$OID" | pack_as_from_promisor >/dev/null && + delete_object repo "$OID" && + + test_must_fail git -C repo -c repack.writeBitmaps=false \ + repack --drop-filtered --filter=blob:limit=1k --dry-run -a 2>err && + test_grep "referenced by the current index" err +' + +test_done diff --git a/t/t7810-grep.sh b/t/t7810-grep.sh index ac7ea9cf8d0268..ffa5f350250d0f 100755 --- a/t/t7810-grep.sh +++ b/t/t7810-grep.sh @@ -89,6 +89,10 @@ test_expect_success setup ' function dummy() {} EOF printf "\200\nASCII\n" >invalid-utf8 && + printf "before\346world\n" >invalid-utf8-embedded && + printf "a\346b\347c\n" >invalid-utf8-multi && + printf "\346world\n" >invalid-utf8-leading && + printf "before\346\n" >invalid-utf8-trailing && if test_have_prereq FUNNYNAMES then echo unusual >"\"unusual\" pathname" && @@ -595,6 +599,39 @@ test_expect_success MB_REGEX 'grep two chars in single-char multibyte file' ' LC_ALL=en_US.UTF-8 test_expect_code 1 git grep ".." reverse-question-mark ' +test_expect_success MACOS,MB_REGEX 'grep matches valid text on both sides of invalid UTF-8' ' + LC_ALL=en_US.UTF-8 git grep -h "befo[r]e" invalid-utf8-embedded >actual && + test_cmp invalid-utf8-embedded actual && + LC_ALL=en_US.UTF-8 git grep -h "worl[d]" invalid-utf8-embedded >actual && + test_cmp invalid-utf8-embedded actual && + LC_ALL=en_US.UTF-8 git grep -h -o "worl[d]" invalid-utf8-embedded >actual && + echo world >expected && + test_cmp expected actual +' + +test_expect_success MACOS,MB_REGEX 'grep matches a run between two invalid sequences' ' + LC_ALL=en_US.UTF-8 git grep -h "[b]" invalid-utf8-multi >actual && + test_cmp invalid-utf8-multi actual +' + +test_expect_success MB_REGEX 'grep does not anchor ^ or $ inside an invalid-byte line' ' + test_expect_code 1 env LC_ALL=en_US.UTF-8 \ + git grep -h "^world" invalid-utf8-embedded && + test_expect_code 1 env LC_ALL=en_US.UTF-8 \ + git grep -h "before\$" invalid-utf8-embedded +' + +test_expect_success MACOS,MB_REGEX 'grep anchors ^ and $ at true line ends past invalid UTF-8' ' + LC_ALL=en_US.UTF-8 git grep -h "^before" invalid-utf8-embedded >actual && + test_cmp invalid-utf8-embedded actual && + LC_ALL=en_US.UTF-8 git grep -h "world\$" invalid-utf8-embedded >actual && + test_cmp invalid-utf8-embedded actual && + LC_ALL=en_US.UTF-8 git grep -h "^" invalid-utf8-leading >actual && + test_cmp invalid-utf8-leading actual && + LC_ALL=en_US.UTF-8 git grep -h "\$" invalid-utf8-trailing >actual && + test_cmp invalid-utf8-trailing actual +' + cat >expected <no-subject.patch <<-\EOF && + This is the body. + EOF + test_must_fail git send-email \ + --dry-run \ + --from="Example " \ + --to=nobody@example.com \ + no-subject.patch 2>actual && + cat >expect <<-\EOF && + No '\''Subject:'\'' line in '\''no-subject.patch'\'' + EOF + test_cmp expect actual +' + test_expect_success $PREREQ 'feed two files' ' rm -fr outdir && git format-patch -2 -o outdir && diff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh index fe6c2617acb2fe..d9de2ef0d88bc8 100755 --- a/t/t9300-fast-import.sh +++ b/t/t9300-fast-import.sh @@ -2827,6 +2827,13 @@ test_expect_success 'R: unknown commandline options are rejected' '\ test_must_fail git fast-import --non-existing-option < /dev/null ' +test_expect_success 'R: feature-only names are rejected on the command line' ' + for opt in --alias --get-mark --ls --notes + do + test_must_fail git fast-import "$opt" file1 && echo more >file2 && git add file1 file2 && + echo untracked >file3 && + echo untracked >ufile && git commit -m one && git branch mybranch && git tag mytag @@ -2712,6 +2714,118 @@ test_expect_success 'git -C checkout uses the right repo' ' EOF ' +test_expect_success 'git checkout completes tracked paths when no refs match' ' + # file1 and file2 are tracked but file3 is not + # there is no ref that begins with f + test_completion "git checkout f" <<-\EOF && + file1 + file2 + EOF + test_completion "git checkout -- f" <<-\EOF + file1 + file2 + EOF +' + +test_expect_success 'git checkout completes untracked paths, too' ' + # ufile is not tracked and there is no ref that begins with u + test_completion "git checkout u" <<-\EOF && + ufile + EOF + test_completion "git checkout -- u" <<-\EOF + ufile + EOF +' + +test_expect_success 'git -C checkout completes paths in specified repo' ' + # otherfile is tracked, oops is not + # lostfile is tracked but lost, ufile is untracked. + test_when_finished "rm -rf repo-for-checkout" && + git init repo-for-checkout && + echo content >repo-for-checkout/otherfile && + echo content >repo-for-checkout/lostfile && + git -C repo-for-checkout add otherfile && + git -C repo-for-checkout add lostfile && + git -C repo-for-checkout commit -m otherfile && + echo untracked >repo-for-checkout/oops && + echo untracked >repo-for-checkout/ufile && + rm -f repo-for-checkout/lostfile && + test_completion "git -C repo-for-checkout checkout o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-checkout checkout -- o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-checkout checkout l" <<-\EOF && + lostfile + EOF + test_completion "git -C repo-for-checkout checkout -- l" <<-\EOF && + lostfile + EOF + test_completion "git -C repo-for-checkout checkout u" <<-\EOF && + ufile + EOF + test_completion "git -C repo-for-checkout checkout -- u" <<-\EOF + ufile + EOF +' + +test_expect_success 'git diff completes tracked paths when no refs match' ' + # file1 and file2 are tracked but file3 is not + # there is no ref that begins with f + test_completion "git diff f" <<-\EOF && + file1 + file2 + EOF + test_completion "git diff -- f" <<-\EOF + file1 + file2 + EOF +' + +test_expect_success 'git diff [--] completes untracked paths, too' ' + # ufile is not tracked and there is no ref that begins with u + test_completion "git diff u" <<-\EOF && + ufile + EOF + test_completion "git diff -- u" <<-\EOF + ufile + EOF +' + +test_expect_success 'git -C diff completes paths in specified repo' ' + test_when_finished "rm -rf repo-for-diff" && + git init repo-for-diff && + echo content >repo-for-diff/otherfile && + echo content >repo-for-diff/lostfile && + git -C repo-for-diff add otherfile && + git -C repo-for-diff add lostfile && + git -C repo-for-diff commit -m otherfile && + echo untracked >repo-for-diff/oops && + echo untracked >repo-for-diff/ufile && + rm -f repo-for-diff/lostfile && + + test_completion "git -C repo-for-diff diff o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-diff diff l" <<-\EOF && + lostfile + EOF + test_completion "git -C repo-for-diff diff u" <<-\EOF && + ufile + EOF + + test_completion "git -C repo-for-diff diff -- o" <<-\EOF && + otherfile + EOF + test_completion "git -C repo-for-diff diff -- l" <<-\EOF && + lostfile + EOF + test_completion "git -C repo-for-diff diff -- u" <<-\EOF + ufile + EOF +' + test_expect_success 'show completes all refs' ' test_completion "git show m" <<-\EOF main Z diff --git a/t/test-lib-functions.sh b/t/test-lib-functions.sh index 03bf31d8efea30..ed9779b0f73310 100644 --- a/t/test-lib-functions.sh +++ b/t/test-lib-functions.sh @@ -2004,6 +2004,41 @@ test_trace2_data () { grep -e '"category":"'"$1"'","key":"'"$2"'","value":"'"$3"'"' } +# Check that the given trace2 data event has the expected value and +# appears exactly once. Produces a diagnostic on failure. +# +# test_trace2_data_singular [