* whitespace=!indent,trail,space
*.[ch] whitespace=indent,trail,space diff=cpp
*.sh whitespace=indent,trail,space eol=lf
-*.perl eol=lf
-*.pm eol=lf
+*.perl eol=lf diff=perl
+*.pl eof=lf diff=perl
+*.pm eol=lf diff=perl
+*.py eol=lf diff=python
/Documentation/git-*.txt eol=lf
/command-list.txt eol=lf
/GIT-VERSION-GEN eol=lf
Benoit Sigoure <tsunanet@gmail.com> <tsuna@lrde.epita.fr>
Bernt Hansen <bernt@norang.ca> <bernt@alumni.uwaterloo.ca>
Brandon Casey <drafnel@gmail.com> <casey@nrlssc.navy.mil>
-brian m. carlson <sandals@crustytoothpaste.ath.cx> Brian M. Carlson <sandals@crustytoothpaste.ath.cx>
-brian m. carlson <sandals@crustytoothpaste.ath.cx> <sandals@crustytoothpaste.net>
+brian m. carlson <sandals@crustytoothpaste.net> Brian M. Carlson <sandals@crustytoothpaste.ath.cx>
+brian m. carlson <sandals@crustytoothpaste.net> <sandals@crustytoothpaste.ath.cx>
Bryan Larsen <bryan@larsen.st> <bryan.larsen@gmail.com>
Bryan Larsen <bryan@larsen.st> <bryanlarsen@yahoo.com>
Cheng Renquan <crquan@gmail.com>
addons:
apt:
+ sources:
+ - ubuntu-toolchain-r-test
packages:
- language-pack-is
- git-svn
- apache2
+ - gcc-8
matrix:
include:
ASCIIDOC_CONF =
ASCIIDOC_HTML = xhtml5
ASCIIDOC_DOCBOOK = docbook45
-ASCIIDOC_EXTRA += -acompat-mode
+ASCIIDOC_EXTRA += -acompat-mode -atabsize=8
ASCIIDOC_EXTRA += -I. -rasciidoctor-extensions
ASCIIDOC_EXTRA += -alitdd='&\#x2d;&\#x2d;'
DBLATEX_COMMON =
--- /dev/null
+Git v2.13.7 Release Notes
+=========================
+
+Fixes since v2.13.6
+-------------------
+
+ * Submodule "names" come from the untrusted .gitmodules file, but we
+ blindly append them to $GIT_DIR/modules to create our on-disk repo
+ paths. This means you can do bad things by putting "../" into the
+ name. We now enforce some rules for submodule names which will cause
+ Git to ignore these malicious names (CVE-2018-11235).
+
+ Credit for finding this vulnerability and the proof of concept from
+ which the test script was adapted goes to Etienne Stalmans.
+
+ * It was possible to trick the code that sanity-checks paths on NTFS
+ into reading random piece of memory (CVE-2018-11233).
+
+Credit for fixing for these bugs goes to Jeff King, Johannes
+Schindelin and others.
--- /dev/null
+Git v2.14.4 Release Notes
+=========================
+
+This release is to forward-port the fixes made in the v2.13.7 version
+of Git. See its release notes for details.
* Clarify and enhance documentation for "merge-base --fork-point", as
it was clear what it computed but not why/what for.
+ * This release also contains the fixes made in the v2.13.7 version of
+ Git. See its release notes for details.
+
Also contains various documentation updates and code clean-ups.
--- /dev/null
+Git v2.16.4 Release Notes
+=========================
+
+This release is to forward-port the fixes made in the v2.13.7 version
+of Git. See its release notes for details.
--- /dev/null
+Git v2.17.1 Release Notes
+=========================
+
+Fixes since v2.17
+-----------------
+
+ * This release contains the same fixes made in the v2.13.7 version of
+ Git, covering CVE-2018-11233 and 11235, and forward-ported to
+ v2.14.4, v2.15.2 and v2.16.4 releases. See release notes to
+ v2.13.7 for details.
+
+ * In addition to the above fixes, this release has support on the
+ server side to reject pushes to repositories that attempt to create
+ such problematic .gitmodules file etc. as tracked contents, to help
+ hosting sites protect their customers by preventing malicious
+ contents from spreading.
"feature" to fetch only commits and/or trees, which nobody used.
This has been removed.
+ * The functionality of "$GIT_DIR/info/grafts" has been superseded by
+ the "refs/replace/" mechanism for some time now, but the internal
+ code had support for it in many places, which has been cleaned up
+ in order to drop support of the "grafts" mechanism.
+
+ * "git worktree add" learned to check out an existing branch.
+
+ * "git --no-pager cmd" did not have short-and-sweet single letter
+ option. Now it does as "-P".
+ (merge 7213c28818 js/no-pager-shorthand later to maint).
+
+ * "git rebase" learned "--rebase-merges" to transplant the whole
+ topology of commit graph elsewhere.
+
+ * "git status" learned to pay attention to UI related diff
+ configuration variables such as diff.renames.
+
+ * The command line completion mechanism (in contrib/) learned to load
+ custom completion file for "git $command" where $command is a
+ custom "git-$command" that the end user has on the $PATH when using
+ newer version of bash.
+
+ * "git send-email" can sometimes offer confirmation dialog "Send this
+ email?" with choices 'Yes', 'No', 'Quit', and 'All'. A new action
+ 'Edit' has been added to this dialog's choice.
+
+ * With merge.renames configuration set to false, the recursive merge
+ strategy can be told not to spend cycles trying to find renamed
+ paths and merge them accordingly.
+
+ * "git status" learned to honor a new status.renames configuration to
+ skip rename detection, which could be useful for those who want to
+ do so without disabling the default rename detection done by the
+ "git diff" command.
+
+ * Command line completion (in contrib/) learned to complete pathnames
+ for various commands better.
+
+ * "git blame" learns to unhighlight uninteresting metadata from the
+ originating commit on lines that are the same as the previous one,
+ and also paint lines in different colors depending on the age of
+ the commit.
+
+ * Transfer protocol v2 learned to support the partial clone.
+
+ * When a short hexadecimal string is used to name an object but there
+ are multiple objects that share the string as the prefix of their
+ names, the code lists these ambiguous candidates in a help message.
+ These object names are now sorted according to their types for
+ easier eyeballing.
+
Performance, Internal Implementation, Development Support etc.
* The code to interface to GPG has been restructured somewhat to make
it cleaner to integrate with other types of signature systems later.
+ * The code has been taught to use the duplicated information stored
+ in the commit-graph file to learn the tree object name for a commit
+ to avoid opening and parsing the commit object when it makes sense
+ to do so.
+
+ * "git gc" in a large repository takes a lot of time as it considers
+ to repack all objects into one pack by default. The command has
+ been taught to pretend as if the largest existing packfile is
+ marked with ".keep" so that it is left untouched while objects in
+ other packs and loose ones are repacked.
+
+ * The transport protocol v2 is getting updated further.
+
+ * The codepath around object-info API has been taught to take the
+ repository object (which in turn tells the API which object store
+ the objects are to be located).
+
+ * Rename detection logic in "diff" family that is used in "merge" has
+ learned to guess when all of x/a, x/b and x/c have moved to z/a,
+ z/b and z/c, it is likely that x/d added in the meantime would also
+ want to move to z/d by taking the hint that the entire directory
+ 'x' moved to 'z'. A bug causing dirty files involved in a rename
+ to be overwritten during merge has also been fixed as part of this
+ work. Incidentally, this also avoids updating a file in the
+ working tree after a (non-trivial) merge whose result matches what
+ our side originally had.
+
+ * "git pack-objects" needs to allocate tons of "struct object_entry"
+ while doing its work, and shrinking its size helps the performance
+ quite a bit.
+
+ * The implementation of "git rebase -i --root" has been updated to use
+ the sequencer machinery more.
+
+ * Developer support update, by using BUG() macro instead of die() to
+ mark codepaths that should not happen more clearly.
+
+ * Developer support. Use newer GCC on one of the builds done at
+ TravisCI.org to get more warnings and errors diagnosed.
+
+ * Conversion from uchar[20] to struct object_id continues.
+
Also contains various documentation updates and code clean-ups.
some merge commits in certain cases, which has been corrected.
(merge be011bbe00 ma/fast-export-skip-merge-fix later to maint).
+ * The code did not propagate the terminal width to subprocesses via
+ COLUMNS environment variable, which it now does. This caused
+ trouble to "git column" helper subprocess when "git tag --column=row"
+ tried to list the existing tags on a display with non-default width.
+ (merge b5d5a567fb nd/term-columns later to maint).
+
+ * We learned that our source files with ".pl" and ".py" extensions
+ are Perl and Python files respectively and changes to them are
+ better viewed as such with appropriate diff drivers.
+ (merge 7818b619e2 ab/perl-python-attrs later to maint).
+
+ * "git rebase -i" sometimes left intermediate "# This is a
+ combination of N commits" message meant for the human consumption
+ inside an editor in the final result in certain corner cases, which
+ has been fixed.
+ (merge 15ef69314d js/rebase-i-clean-msg-after-fixup-continue later to maint).
+
+ * A test to see if the filesystem normalizes UTF-8 filename has been
+ updated to check what we need to know in a more direct way, i.e. a
+ path created in NFC form can be accessed with NFD form (or vice
+ versa) to cope with APFS as well as HFS.
+ (merge 742ae10e35 tb/test-apfs-utf8-normalization later to maint).
+
+ * "git format-patch --cover --attach" created a broken MIME multipart
+ message for the cover letter, which has been fixed by keeping the
+ cover letter as plain text file.
+ (merge 50cd54ef4e bc/format-patch-cover-no-attach later to maint).
+
+ * The split-index feature had a long-standing and dormant bug in
+ certain use of the in-core merge machinery, which has been fixed.
+ (merge 7db118303a en/unpack-trees-split-index-fix later to maint).
+
+ * Asciidoctor gives a reasonable imitation for AsciiDoc, but does not
+ render illustration in a literal block correctly when indented with
+ HT by default. The problem is fixed by forcing 8-space tabs.
+ (merge 379805051d bc/asciidoctor-tab-width later to maint).
+
+ * Code clean-up to adjust to a more recent lockfile API convention that
+ allows lockfile instances kept on the stack.
+ (merge 0fa5a2ed8d ma/lockfile-cleanup later to maint).
+
+ * the_repository->index is not a allocated piece of memory but
+ repo_clear() indiscriminately attempted to free(3) it, which has
+ been corrected.
+ (merge 74373b5f10 nd/repo-clear-keep-the-index later to maint).
+
+ * Code clean-up to avoid non-standard-conformant pointer arithmetic.
+ (merge c112084af9 rs/no-null-ptr-arith-in-fast-export later to maint).
+
+ * Code clean-up to turn history traversal more robust in a
+ semi-corrupt repository.
+ (merge 8702b30fd7 jk/unavailable-can-be-missing later to maint).
+
+ * "git update-ref A B" is supposed to ensure that ref A does not yet
+ exist when B is a NULL OID, but this check was not done correctly
+ for pseudo-refs outside refs/ hierarchy, e.g. MERGE_HEAD.
+
+ * "git submodule update" and "git submodule add" supported the
+ "--reference" option to borrow objects from a neighbouring local
+ repository like "git clone" does, but lacked the more recent
+ invention "--dissociate". Also "git submodule add" has been taught
+ to take the "--progress" option.
+ (merge a0ef29341a cf/submodule-progress-dissociate later to maint).
+
+ * Update credential-netrc helper (in contrib/) to allow customizing
+ the GPG used to decrypt the encrypted .netrc file.
+ (merge 786ef50a23 lm/credential-netrc later to maint).
+
+ * "git submodule update" attempts two different kinds of "git fetch"
+ against the upstream repository to grab a commit bound at the
+ submodule's path, but it incorrectly gave up if the first kind
+ (i.e. a normal fetch) failed, making the second "last resort" one
+ (i.e. fetching an exact commit object by object name) ineffective.
+ This has been corrected.
+ (merge e30d833671 sb/submodule-update-try-harder later to maint).
+
* Other minor doc, test and build updates and code cleanups.
(merge 248f66ed8e nd/trace-with-env later to maint).
(merge 14ced5562c ys/bisect-object-id-missing-conversion-fix later to maint).
(merge adc887221f tq/t1510 later to maint).
(merge bed21a8ad6 sg/doc-gc-quote-mismatch-fix later to maint).
(merge 73364e4f10 tz/doc-git-urls-reference later to maint).
+ (merge cd1e606bad bc/mailmap-self later to maint).
+ (merge f7997e3682 ao/config-api-doc later to maint).
+ (merge ee930754d8 jk/apply-p-doc later to maint).
+ (merge 011b648646 nd/pack-format-doc later to maint).
+ (merge 87a6bb701a sg/t5310-jgit-bitmap-test later to maint).
+ (merge f6b82970aa sg/t5516-fixes later to maint).
+ (merge 4362da078e sg/t7005-spaces-in-filenames-cleanup later to maint).
+ (merge 7d0ee47c11 js/test-unset-prereq later to maint).
+ (merge 5356a3c354 ah/misc-doc-updates later to maint).
+ (merge 92c4a7a129 nd/completion-aliasfiletype-typofix later to maint).
+ (merge 58bd77b66a nd/pack-unreachable-objects-doc later to maint).
+ (merge 4ed79d5203 sg/t6500-no-redirect-of-stdin later to maint).
"git pull" is run. See "pull.rebase" for doing this in a non
branch-specific manner.
+
+When `merges`, pass the `--rebase-merges` option to 'git rebase'
+so that the local merge commits are included in the rebase (see
+linkgit:git-rebase[1] for details).
++
When preserve, also pass `--preserve-merges` along to 'git rebase'
so that locally committed merge commits will not be flattened
by running 'git pull'.
status short-format), or
`unmerged` (files which have unmerged changes).
+color.blame.repeatedLines::
+ Use the customized color for the part of git-blame output that
+ is repeated meta information per line (such as commit id,
+ author name, date and timezone). Defaults to cyan.
+
+color.blame.highlightRecent::
+ This can be used to color the metadata of a blame line depending
+ on age of the line.
++
+This setting should be set to a comma-separated list of color and date settings,
+starting and ending with a color, the dates should be set from oldest to newest.
+The metadata will be colored given the colors if the the line was introduced
+before the given timestamp, overwriting older timestamped colors.
++
+Instead of an absolute timestamp relative timestamps work as well, e.g.
+2.weeks.ago is valid to address anything older than 2 weeks.
++
+It defaults to 'blue,12 month ago,white,1 month ago,red', which colors
+everything older than one year blue, recent changes between one month and
+one year old are kept white, and lines introduced within the last month are
+colored red.
+
+blame.coloring::
+ This determines the coloring scheme to be applied to blame
+ output. It can be 'repeatedLines', 'highlightRecent',
+ or 'none' which is the default.
+
color.transport::
A boolean to enable/disable color when pushes are rejected. May be
set to `always`, `false` (or `never`) or `auto` (or `true`), in which
Make `git gc --auto` return immediately and run in background
if the system supports it. Default is true.
+gc.bigPackThreshold::
+ If non-zero, all packs larger than this limit are kept when
+ `git gc` is run. This is very similar to `--keep-base-pack`
+ except that all packs that meet the threshold are kept, not
+ just the base pack. Defaults to zero. Common unit suffixes of
+ 'k', 'm', or 'g' are supported.
++
+Note that if the number of kept packs is more than gc.autoPackLimit,
+this configuration variable is ignored, all packs except the base pack
+will be repacked. After this the number of packs should go below
+gc.autoPackLimit and gc.bigPackThreshold should be respected again.
+
gc.logExpiry::
If the file gc.log exists, then `git gc --auto` won't run
unless that file is more than 'gc.logExpiry' old. Default is
pack.depth::
The maximum delta depth used by linkgit:git-pack-objects[1] when no
maximum depth is given on the command line. Defaults to 50.
+ Maximum value is 4095.
pack.windowMemory::
The maximum size of memory that is consumed by each thread
The maximum size of a delta, that is cached in
linkgit:git-pack-objects[1]. This cache is used to speed up the
writing object phase by not having to recompute the final delta
- result once the best match for all objects is found. Defaults to 1000.
+ result once the best match for all objects is found.
+ Defaults to 1000. Maximum value is 65535.
pack.threads::
Specifies the number of threads to spawn when searching for best
pull" is run. See "branch.<name>.rebase" for setting this on a
per-branch basis.
+
+When `merges`, pass the `--rebase-merges` option to 'git rebase'
+so that the local merge commits are included in the rebase (see
+linkgit:git-rebase[1] for details).
++
When preserve, also pass `--preserve-merges` along to 'git rebase'
so that locally committed merge commits will not be flattened
by running 'git pull'.
behavior of linkgit:git-status[1] in Git 1.8.4 and previous.
Defaults to false.
+status.renameLimit::
+ The number of files to consider when performing rename detection
+ in linkgit:git-status[1] and linkgit:git-commit[1]. Defaults to
+ the value of diff.renameLimit.
+
+status.renames::
+ Whether and how Git detects renames in linkgit:git-status[1] and
+ linkgit:git-commit[1] . If set to "false", rename detection is
+ disabled. If set to "true", basic rename detection is enabled.
+ If set to "copies" or "copy", Git will detect copies, as well.
+ Defaults to the value of diff.renames.
+
status.showStash::
If set to true, linkgit:git-status[1] will display the number of
entries currently stashed away.
diff.renameLimit::
The number of files to consider when performing the copy/rename
- detection; equivalent to the 'git diff' option `-l`.
+ detection; equivalent to the 'git diff' option `-l`. This setting
+ has no effect if rename detection is turned off.
diff.renames::
Whether and how Git detects renames. If set to "false",
is specified. This flag forces progress status even if the
standard error stream is not directed to a terminal.
+-o <option>::
+--server-option=<option>::
+ Transmit the given string to the server when communicating using
+ protocol version 2. The given string must not contain a NUL or LF
+ character.
+ When multiple `--server-option=<option>` are given, they are all
+ sent to the other side in the order listed on the command line.
+
-4::
--ipv4::
Use IPv4 addresses only, ignoring IPv6 addresses.
for command-line options).
-Configuration
+CONFIGURATION
-------------
The optional configuration variable `core.excludesFile` indicates a path to a
listing the files explicitly), it does not consider
`subdir/git-foo.sh`.
-Interactive mode
+INTERACTIVE MODE
----------------
When the command enters the interactive mode, it shows the
output of the 'status' subcommand, and then goes into its
linkgit:git-config[1]).
-p<n>::
- Remove <n> leading slashes from traditional diff paths. The
- default is 1.
+ Remove <n> leading path components (separated by slashes) from
+ traditional diff paths. E.g., with `-p2`, a patch against
+ `a/dir/file` will be applied directly to `file`. The default is
+ 1.
-C<n>::
Ensure at least <n> lines of surrounding context match before
the `--unsafe-paths` option to override this safety check. This option
has no effect when `--index` or `--cached` is in use.
-Configuration
+CONFIGURATION
-------------
apply.ignoreWhitespace::
When no `--whitespace` flag is given from the command
line, this configuration item is used as the default.
-Submodules
+SUBMODULES
----------
If the patch contains any changes to submodules then 'git apply'
treats these changes as follows.
`--list` is used or implied. The default is to use a pager.
See linkgit:git-config[1].
-Examples
+EXAMPLES
--------
Start development from a known tag::
is currently checked out) does not have all commits from the test branch.
-Notes
+NOTES
-----
If you are creating a branch that you want to checkout immediately, it is
to contain objects already in the destination, as these are ignored
when unpacking at the destination.
-EXAMPLE
--------
+EXAMPLES
+--------
Assume you want to transfer the history from a repository R1 on machine A
to another repository R2 on machine B.
:git-clone: 1
include::urls.txt[]
-Examples
+EXAMPLES
--------
* Clone from upstream:
------
[[dbbackend]]
-Database Backend
+DATABASE BACKEND
----------------
'git-cvsserver' uses one database per Git head (i.e. CVS module) to
When these environment variables are set, the corresponding
command-line arguments may not be used.
-Eclipse CVS Client Notes
+ECLIPSE CVS CLIENT NOTES
------------------------
To get a checkout with the Eclipse CVS client:
the cvs utility on the server with 'git-cvsserver' or manipulate your `.bashrc`
so that calling 'cvs' effectively calls 'git-cvsserver'.
-Clients known to work
+CLIENTS KNOWN TO WORK
---------------------
- CVS 1.12.9 on Debian
- Eclipse 3.0, 3.1.2 on MacOSX (see Eclipse CVS Client Notes)
- TortoiseCVS
-Operations supported
+OPERATIONS SUPPORTED
--------------------
All the operations required for normal use are supported, including
defaults by setting `gitcvs.usecrlfattr` to true,
and `gitcvs.allBinary` to "guess".
-Dependencies
+DEPENDENCIES
------------
'git-cvsserver' depends on DBD::SQLite.
include::diff-format.txt[]
-Operating Modes
+OPERATING MODES
---------------
You can choose whether you want to trust the index file entirely
(using the `--cached` flag) or ask the diff logic to show any files
that don't match the stat state as being "tentatively changed". Both
of these operations are very useful indeed.
-Cached Mode
+CACHED MODE
-----------
If `--cached` is specified, it allows you to ask:
asking yourself "what have I already marked for being committed, and
what's the difference to a previous tree".
-Non-cached Mode
+NON-CACHED MODE
---------------
The "non-cached" mode takes a different approach, and is potentially
the more useful of the two in that what it does can't be emulated with
include::pretty-formats.txt[]
-Limiting Output
+LIMITING OUTPUT
---------------
If you're only interested in differences in a subset of files, for
example some architecture-specific files, you might do:
'git diff' [options] --cached [<commit>] [--] [<path>...]
'git diff' [options] <commit> <commit> [--] [<path>...]
'git diff' [options] <blob> <blob>
-'git diff' [options] [--no-index] [--] <path> <path>
+'git diff' [options] --no-index [--] <path> <path>
DESCRIPTION
-----------
between the index and a tree, changes between two trees, changes between
two blob objects, or changes between two files on disk.
-'git diff' [--options] [--] [<path>...]::
+'git diff' [options] [--] [<path>...]::
This form is to view the changes you made relative to
the index (staging area for the next commit). In other
further add to the index but you still haven't. You can
stage these changes by using linkgit:git-add[1].
-'git diff' --no-index [--options] [--] [<path>...]::
+'git diff' [options] --no-index [--] <path> <path>::
This form is to compare the given two paths on the
filesystem. You can omit the `--no-index` option when
or when running the command outside a working tree
controlled by Git.
-'git diff' [--options] --cached [<commit>] [--] [<path>...]::
+'git diff' [options] --cached [<commit>] [--] [<path>...]::
This form is to view the changes you staged for the next
commit relative to the named <commit>. Typically you
<commit> is not given, it shows all staged changes.
--staged is a synonym of --cached.
-'git diff' [--options] <commit> [--] [<path>...]::
+'git diff' [options] <commit> [--] [<path>...]::
This form is to view the changes you have in your
working tree relative to the named <commit>. You can
branch name to compare with the tip of a different
branch.
-'git diff' [--options] <commit> <commit> [--] [<path>...]::
+'git diff' [options] <commit> <commit> [--] [<path>...]::
This is to view the changes between two arbitrary
<commit>.
-'git diff' [--options] <commit>..<commit> [--] [<path>...]::
+'git diff' [options] <commit>..<commit> [--] [<path>...]::
This is synonymous to the previous form. If <commit> on
one side is omitted, it will have the same effect as
using HEAD instead.
-'git diff' [--options] <commit>\...<commit> [--] [<path>...]::
+'git diff' [options] <commit>\...<commit> [--] [<path>...]::
This form is to view the changes on the branch containing
and up to the second <commit>, starting at a common ancestor
no private data in the stream.
-Limitations
+LIMITATIONS
-----------
Since 'git fast-import' cannot tag trees, you will not be
fastimport.unpackLimit::
See linkgit:git-config[1]
-Performance
+PERFORMANCE
-----------
The design of fast-import allows it to import large projects in a minimum
amount of memory usage and processing time. Assuming the frontend
destination Git repository (due to less IO contention).
-Development Cost
+DEVELOPMENT COST
----------------
A typical frontend for fast-import tends to weigh in at approximately 200
lines of Perl/Python/Ruby code. Most developers have been able to
(use once, and never look back).
-Parallel Operation
+PARALLEL OPERATION
------------------
Like 'git push' or 'git fetch', imports handled by fast-import are safe to
run alongside parallel `git repack -a -d` or `git gc` invocations,
is not necessary for an initial import into an empty repository.
-Technical Discussion
+TECHNICAL DISCUSSION
--------------------
fast-import tracks a set of branches in memory. Any branch can be created
or modified at any point during the import process by sending a
need to perform any costly file update operations when switching
between branches.
-Input Format
+INPUT FORMAT
------------
With the exception of raw file data (which Git does not interpret)
the fast-import input format is text (ASCII) based. This text based
in use, the `done` command is mandatory and marks the end of the
stream.
-Responses To Commands
+RESPONSES TO COMMANDS
---------------------
New objects written by fast-import are not available immediately.
Most fast-import commands have no visible effect until the next
pending output from `progress`, `ls`, `get-mark`, and `cat-blob` before
performing writes to fast-import that might block.
-Crash Reports
+CRASH REPORTS
-------------
If fast-import is supplied invalid input it will terminate with a
non-zero exit status and create a crash report in the top level of
END OF CRASH REPORT
====
-Tips and Tricks
+TIPS AND TRICKS
---------------
The following tips and tricks have been collected from various
users of fast-import, and are offered here as suggestions.
has been processed.
-Packfile Optimization
+PACKFILE OPTIMIZATION
---------------------
When packing a blob fast-import always attempts to deltify against the last
blob written. Unless specifically arranged for by the frontend,
final packfile size (30-50% smaller can be quite typical).
-Memory Utilization
+MEMORY UTILIZATION
------------------
There are a number of factors which affect how much memory fast-import
requires to perform an import. Like critical sections of core
projects with 2,000+ branches and 45,114+ files in a very limited
memory footprint (less than 2.7 MiB per active branch).
-Signals
+SIGNALS
-------
Sending *SIGUSR1* to the 'git fast-import' process ends the current
packfile early, simulating a `checkpoint` command. The impatient
any other non-zero value.
-Examples
+EXAMPLES
--------
Suppose you want to remove a file (containing confidential information
or even simpler:
-----------------------------------------------
-echo "$commit-id $graft-id" >> .git/info/grafts
+git replace --graft $commit-id $graft-id
git filter-branch $graft-id..HEAD
-----------------------------------------------
-Checklist for Shrinking a Repository
+CHECKLIST FOR SHRINKING A REPOSITORY
------------------------------------
git-filter-branch can be used to get rid of a subset of files,
(or if your git-gc is not new enough to support arguments to
`--prune`, use `git repack -ad; git prune` instead).
-Notes
+NOTES
-----
git-filter-branch allows you to make complex shell-scripted rewrites
Synonym to `merge.log`; this is deprecated and will be removed in
the future.
-EXAMPLE
--------
+EXAMPLES
+--------
---------
$ git fetch origin master
SYNOPSIS
--------
[verse]
-'git gc' [--aggressive] [--auto] [--quiet] [--prune=<date> | --no-prune] [--force]
+'git gc' [--aggressive] [--auto] [--quiet] [--prune=<date> | --no-prune] [--force] [--keep-largest-pack]
DESCRIPTION
-----------
to 0 disables automatic packing of loose objects.
+
If the number of packs exceeds the value of `gc.autoPackLimit`,
-then existing packs (except those marked with a `.keep` file)
+then existing packs (except those marked with a `.keep` file
+or over `gc.bigPackThreshold` limit)
are consolidated into a single pack by using the `-A` option of
-'git repack'. Setting `gc.autoPackLimit` to 0 disables
-automatic consolidation of packs.
+'git repack'.
+If the amount of memory is estimated not enough for `git repack` to
+run smoothly and `gc.bigPackThreshold` is not set, the largest
+pack will also be excluded (this is the equivalent of running `git gc`
+with `--keep-base-pack`).
+Setting `gc.autoPackLimit` to 0 disables automatic consolidation of
+packs.
+
If houskeeping is required due to many loose objects or packs, all
other housekeeping tasks (e.g. rerere, working trees, reflog...) will
Force `git gc` to run even if there may be another `git gc`
instance running on this repository.
-Configuration
+--keep-largest-pack::
+ All packs except the largest pack and those marked with a
+ `.keep` files are consolidated into a single pack. When this
+ option is used, `gc.bigPackThreshold` is ignored.
+
+CONFIGURATION
-------------
The optional configuration variable `gc.reflogExpire` can be
it. Default is "3 months ago".
-Notes
+NOTES
-----
'git gc' tries very hard not to delete objects that are referenced
For more details about the <pathspec> syntax, see the 'pathspec' entry
in linkgit:gitglossary[7].
-Examples
+EXAMPLES
--------
`git grep 'time_t' -- '*.[ch]'`::
The remote refs to update.
-Specifying the Refs
+SPECIFYING THE REFS
-------------------
A '<ref>' specification can be either a single pattern, or a pair
.........................
-EXAMPLE
--------
+EXAMPLES
+--------
To submit patches using GMail's IMAP interface, first, edit your ~/.gitconfig
to specify your account settings:
--max-input-size=<size>::
Die, if the pack is larger than <size>.
-Note
-----
+NOTES
+-----
Once the index has been created, the list of object names is sorted
and the SHA-1 hash of that list is printed to stdout. If --stdin was
Show only ignored files in the output. When showing files in the
index, print only those matched by an exclude pattern. When
showing "other" files, show only those matched by an exclude
- pattern.
+ pattern. Standard ignore rules are not automatically activated,
+ therefore at least one of the `--exclude*` options is required.
-s::
--stage::
Files to show. If no files are given all files which match the other
specified criteria are shown.
-Output
+OUTPUT
------
'git ls-files' just outputs the filenames unless `--stage` is specified in
which case it outputs:
verbatim and the line is terminated by a NUL byte.
-Exclude Patterns
+EXCLUDE PATTERNS
----------------
'git ls-files' can use a list of "exclude patterns" when
themselves will not work for refs whose objects have not yet been
fetched from the remote, and will give a `missing object` error.
+-o <option>::
+--server-option=<option>::
+ Transmit the given string to the server when communicating using
+ protocol version 2. The given string must not contain a NUL or LF
+ character.
+ When multiple `--server-option=<option>` are given, they are all
+ sent to the other side in the order listed on the command line.
+
<repository>::
The "remote" repository to query. This parameter can be
either a URL or the name of a remote (see the GIT URLS and
--always::
Show uniquely abbreviated commit object as fallback.
-EXAMPLE
--------
+EXAMPLES
+--------
Given a commit, find out where it is relative to the local refs. Say somebody
wrote you about that fantastic commit 33db5f4d9027a10e477ccf054b2c1ab94f74c85a.
the updated p4 remote branch.
-EXAMPLE
--------
+EXAMPLES
+--------
* Clone a repository:
+
------------
'git pack-objects' [-q | --progress | --all-progress] [--all-progress-implied]
[--no-reuse-delta] [--delta-base-offset] [--non-empty]
[--local] [--incremental] [--window=<n>] [--depth=<n>]
- [--revs [--unpacked | --all]]
+ [--revs [--unpacked | --all]] [--keep-pack=<pack-name>]
[--stdout [--filter=<filter-spec>] | base-name]
[--shallow] [--keep-true-parents] < object-list
it too deep affects the performance on the unpacker
side, because delta data needs to be applied that many
times to get to the necessary object.
- The default value for --window is 10 and --depth is 50.
++
+The default value for --window is 10 and --depth is 50. The maximum
+depth is 4095.
--window-memory=<n>::
This option provides an additional limit on top of `--window`;
has a .keep file to be ignored, even if it would have
otherwise been packed.
+--keep-pack=<pack-name>::
+ This flag causes an object already in the given pack to be
+ ignored, even if it would have otherwise been
+ packed. `<pack-name>` is the the pack file name without
+ leading directory (e.g. `pack-123.pack`). The option could be
+ specified multiple times to keep multiple packs.
+
--incremental::
This flag causes an object already in a pack to be ignored
even if it would have otherwise been packed.
locally created objects [without .promisor] and objects from the
promisor remote [with .promisor].) This is used with partial clone.
+--keep-unreachable::
+ Objects unreachable from the refs in packs named with
+ --unpacked= option are added to the resulting pack, in
+ addition to the reachable objects that are not in packs marked
+ with *.keep files. This implies `--revs`.
+
+--pack-loose-unreachable::
+ Pack unreachable loose objects (and their loose counterparts
+ removed). This implies `--revs`.
+
+--unpack-unreachable::
+ Keep unreachable objects in loose form. This implies `--revs`.
+
SEE ALSO
--------
linkgit:git-rev-list[1]
reachable from any of our references, keep objects
reachable from listed <head>s.
-EXAMPLE
--------
+EXAMPLES
+--------
To prune objects not used by your repository or another that
borrows from your repository via its
$ git prune $(cd ../another && git rev-parse --all)
------------
-Notes
+NOTES
-----
In most cases, users will not need to call 'git prune' directly, but
include::merge-options.txt[]
-r::
---rebase[=false|true|preserve|interactive]::
+--rebase[=false|true|merges|preserve|interactive]::
When true, rebase the current branch on top of the upstream
branch after fetching. If there is a remote-tracking branch
corresponding to the upstream branch and the upstream branch
was rebased since last fetched, the rebase uses that information
to avoid rebasing non-local changes.
+
+When set to `merges`, rebase using `git rebase --rebase-merges` so that
+the local merge commits are included in the rebase (see
+linkgit:git-rebase[1] for details).
++
When set to preserve, rebase with the `--preserve-merges` option passed
to `git rebase` so that locally created merge commits will not be flattened.
+
[verse]
'git push' [--all | --mirror | --tags] [--follow-tags] [--atomic] [-n | --dry-run] [--receive-pack=<git-receive-pack>]
[--repo=<repository>] [-f | --force] [-d | --delete] [--prune] [-v | --verbose]
- [-u | --set-upstream] [--push-option=<string>]
+ [-u | --set-upstream] [-o <string> | --push-option=<string>]
[--[no-]signed|--signed=(true|false|if-asked)]
[--force-with-lease[=<refname>[:<expect>]]]
[--no-verify] [<repository> [<refspec>...]]
will be tab-separated and sent to stdout instead of stderr. The full
symbolic names of the refs will be given.
+-d::
--delete::
All listed refs are deleted from the remote repository. This is
the same as prefixing all refs with a colon.
refs, no explanation is needed. For a failed ref, the reason for
failure is described.
-Note about fast-forwards
+NOTE ABOUT FAST-FORWARDS
------------------------
When an update changes a branch (or more in general, a ref) that used to
a case where you do mean to lose history.
-Examples
+EXAMPLES
--------
`git push`::
The id of the tree object(s) to be read/merged.
-Merging
+MERGING
-------
If `-m` is specified, 'git read-tree' can perform 3 kinds of
merge, a single tree merge if only 1 tree is given, a
have finished your work-in-progress), attempt the merge again.
-Sparse checkout
+SPARSE CHECKOUT
---------------
"Sparse checkout" allows populating the working directory sparsely.
rebase.instructionFormat. A customized instruction format will automatically
have the long commit hash prepended to the format.
+-r::
+--rebase-merges[=(rebase-cousins|no-rebase-cousins)]::
+ By default, a rebase will simply drop merge commits from the todo
+ list, and put the rebased commits into a single, linear branch.
+ With `--rebase-merges`, the rebase will instead try to preserve
+ the branching structure within the commits that are to be rebased,
+ by recreating the merge commits. Any resolved merge conflicts or
+ manual amendments in these merge commits will have to be
+ resolved/re-applied manually.
++
+By default, or when `no-rebase-cousins` was specified, commits which do not
+have `<upstream>` as direct ancestor will keep their original branch point,
+i.e. commits that would be excluded by gitlink:git-log[1]'s
+`--ancestry-path` option will keep their original ancestry by default. If
+the `rebase-cousins` mode is turned on, such commits are instead rebased
+onto `<upstream>` (or `<onto>`, if specified).
++
+The `--rebase-merges` mode is similar in spirit to `--preserve-merges`, but
+in contrast to that option works well in interactive rebases: commits can be
+reordered, inserted and dropped at will.
++
+It is currently only possible to recreate the merge commits using the
+`recursive` merge strategy; Different merge strategies can be used only via
+explicit `exec git merge -s <strategy> [...]` commands.
++
+See also REBASING MERGES below.
+
-p::
--preserve-merges::
Recreate merge commits instead of flattening the history by replaying
'everyone' downstream from 'topic' will now have to perform a "hard
case" recovery too!
+REBASING MERGES
+-----------------
+
+The interactive rebase command was originally designed to handle
+individual patch series. As such, it makes sense to exclude merge
+commits from the todo list, as the developer may have merged the
+then-current `master` while working on the branch, only to rebase
+all the commits onto `master` eventually (skipping the merge
+commits).
+
+However, there are legitimate reasons why a developer may want to
+recreate merge commits: to keep the branch structure (or "commit
+topology") when working on multiple, inter-related branches.
+
+In the following example, the developer works on a topic branch that
+refactors the way buttons are defined, and on another topic branch
+that uses that refactoring to implement a "Report a bug" button. The
+output of `git log --graph --format=%s -5` may look like this:
+
+------------
+* Merge branch 'report-a-bug'
+|\
+| * Add the feedback button
+* | Merge branch 'refactor-button'
+|\ \
+| |/
+| * Use the Button class for all buttons
+| * Extract a generic Button class from the DownloadButton one
+------------
+
+The developer might want to rebase those commits to a newer `master`
+while keeping the branch topology, for example when the first topic
+branch is expected to be integrated into `master` much earlier than the
+second one, say, to resolve merge conflicts with changes to the
+DownloadButton class that made it into `master`.
+
+This rebase can be performed using the `--rebase-merges` option.
+It will generate a todo list looking like this:
+
+------------
+label onto
+
+# Branch: refactor-button
+reset onto
+pick 123456 Extract a generic Button class from the DownloadButton one
+pick 654321 Use the Button class for all buttons
+label refactor-button
+
+# Branch: report-a-bug
+reset refactor-button # Use the Button class for all buttons
+pick abcdef Add the feedback button
+label report-a-bug
+
+reset onto
+merge -C a1b2c3 refactor-button # Merge 'refactor-button'
+merge -C 6f5e4d report-a-bug # Merge 'report-a-bug'
+------------
+
+In contrast to a regular interactive rebase, there are `label`, `reset`
+and `merge` commands in addition to `pick` ones.
+
+The `label` command associates a label with the current HEAD when that
+command is executed. These labels are created as worktree-local refs
+(`refs/rewritten/<label>`) that will be deleted when the rebase
+finishes. That way, rebase operations in multiple worktrees linked to
+the same repository do not interfere with one another. If the `label`
+command fails, it is rescheduled immediately, with a helpful message how
+to proceed.
+
+The `reset` command resets the HEAD, index and worktree to the specified
+revision. It is isimilar to an `exec git reset --hard <label>`, but
+refuses to overwrite untracked files. If the `reset` command fails, it is
+rescheduled immediately, with a helpful message how to edit the todo list
+(this typically happens when a `reset` command was inserted into the todo
+list manually and contains a typo).
+
+The `merge` command will merge the specified revision into whatever is
+HEAD at that time. With `-C <original-commit>`, the commit message of
+the specified merge commit will be used. When the `-C` is changed to
+a lower-case `-c`, the message will be opened in an editor after a
+successful merge so that the user can edit the message.
+
+If a `merge` command fails for any reason other than merge conflicts (i.e.
+when the merge operation did not even start), it is rescheduled immediately.
+
+At this time, the `merge` command will *always* use the `recursive`
+merge strategy, with no way to choose a different one. To work around
+this, an `exec` command can be used to call `git merge` explicitly,
+using the fact that the labels are worktree-local refs (the ref
+`refs/rewritten/onto` would correspond to the label `onto`, for example).
+
+Note: the first command (`label onto`) labels the revision onto which
+the commits are rebased; The name `onto` is just a convention, as a nod
+to the `--onto` option.
+
+It is also possible to introduce completely new merge commits from scratch
+by adding a command of the form `merge <merge-head>`. This form will
+generate a tentative commit message and always open an editor to let the
+user edit it. This can be useful e.g. when a topic branch turns out to
+address more than a single concern and wants to be split into two or
+even more topic branches. Consider this todo list:
+
+------------
+pick 192837 Switch from GNU Makefiles to CMake
+pick 5a6c7e Document the switch to CMake
+pick 918273 Fix detection of OpenSSL in CMake
+pick afbecd http: add support for TLS v1.3
+pick fdbaec Fix detection of cURL in CMake on Windows
+------------
+
+The one commit in this list that is not related to CMake may very well
+have been motivated by working on fixing all those bugs introduced by
+switching to CMake, but it addresses a different concern. To split this
+branch into two topic branches, the todo list could be edited like this:
+
+------------
+label onto
+
+pick afbecd http: add support for TLS v1.3
+label tlsv1.3
+
+reset onto
+pick 192837 Switch from GNU Makefiles to CMake
+pick 918273 Fix detection of OpenSSL in CMake
+pick fdbaec Fix detection of cURL in CMake on Windows
+pick 5a6c7e Document the switch to CMake
+label cmake
+
+reset onto
+merge tlsv1.3
+merge cmake
+------------
+
BUGS
----
The todo list presented by `--preserve-merges --interactive` does not
represent the topology of the revision graph. Editing commits and
rewording their commit messages should work fine, but attempts to
-reorder commits tend to produce counterintuitive results.
+reorder commits tend to produce counterintuitive results. Use
+`--rebase-merges` in such scenarios instead.
For example, an attempt to rearrange
------------
<directory>::
The repository to sync into.
-pre-receive Hook
+PRE-RECEIVE HOOK
----------------
Before any ref is updated, if $GIT_DIR/hooks/pre-receive file exists
and is executable, it will be invoked once with no parameters. The
See the notes on the quarantine environment below.
-update Hook
+UPDATE HOOK
-----------
Before each ref is updated, if $GIT_DIR/hooks/update file exists
and is executable, it is invoked once per ref, with three parameters:
As such it is not a good idea to send notices (e.g. email) from
this hook. Consider using the post-receive hook instead.
-post-receive Hook
+POST-RECEIVE HOOK
-----------------
After all refs were updated (or attempted to be updated), if any
ref update was successful, and if $GIT_DIR/hooks/post-receive
to evaluate it. It is recommended that hooks rely on sha1-new
rather than the current value of refname.
-post-update Hook
+POST-UPDATE HOOK
----------------
After all other processing, if at least one ref was updated, and
if $GIT_DIR/hooks/post-update file exists and is executable, then
exec git update-server-info
-Quarantine Environment
+QUARANTINE ENVIRONMENT
----------------------
When `receive-pack` takes in objects, they are placed into a temporary
the vhost field in the git:// service request (to rest of the argument).
Default is not to send vhost in such request (if sent).
-ENVIRONMENT VARIABLES:
-----------------------
+ENVIRONMENT VARIABLES
+---------------------
GIT_TRANSLOOP_DEBUG::
If set, prints debugging information about various reads/writes.
-ENVIRONMENT VARIABLES PASSED TO COMMAND:
-----------------------------------------
+ENVIRONMENT VARIABLES PASSED TO COMMAND
+---------------------------------------
GIT_EXT_SERVICE::
Set to long name (git-upload-pack, etc...) of service helper needs
to invoke.
-EXAMPLES:
----------
+EXAMPLES
+--------
This remote helper is transparently used by Git when
you use commands such as "git fetch <URL>", "git clone <URL>",
, "git push <URL>" or "git remote add <nick> <URL>", where <URL>
`remote.origin.fetch` configuration variables. (See
linkgit:git-config[1]).
-Examples
+EXAMPLES
--------
* Add a new remote, fetch, and check out a branch from it
SYNOPSIS
--------
[verse]
-'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [--window=<n>] [--depth=<n>] [--threads=<n>]
+'git repack' [-a] [-A] [-d] [-f] [-F] [-l] [-n] [-q] [-b] [--window=<n>] [--depth=<n>] [--threads=<n>] [--keep-pack=<pack-name>]
DESCRIPTION
-----------
space. `--depth` limits the maximum delta depth; making it too deep
affects the performance on the unpacker side, because delta data needs
to be applied that many times to get to the necessary object.
- The default value for --window is 10 and --depth is 50.
++
+The default value for --window is 10 and --depth is 50. The maximum
+depth is 4095.
--threads=<n>::
This option is passed through to `git pack-objects`.
with `-b` or `repack.writeBitmaps`, as it ensures that the
bitmapped packfile has the necessary objects.
+--keep-pack=<pack-name>::
+ Exclude the given pack from repacking. This is the equivalent
+ of having `.keep` file on the pack. `<pack-name>` is the the
+ pack file name without leading directory (e.g. `pack-123.pack`).
+ The option could be specified multiple times to keep multiple
+ packs.
+
--unpack-unreachable=<when>::
When loosening unreachable objects, do not bother loosening any
objects older than `<when>`. This can be used to optimize out
'git replace' [-f] <object> <replacement>
'git replace' [-f] --edit <object>
'git replace' [-f] --graft <commit> [<parent>...]
+'git replace' [-f] --convert-graft-file
'git replace' -d <object>...
'git replace' [--format=<format>] [-l [<pattern>]]
content as <commit> except that its parents will be
[<parent>...] instead of <commit>'s parents. A replacement ref
is then created to replace <commit> with the newly created
- commit. See contrib/convert-grafts-to-replace-refs.sh for an
- example script based on this option that can convert grafts to
- replace refs.
+ commit. Use `--convert-graft-file` to convert a
+ `$GIT_DIR/info/grafts` file and use replace refs instead.
+
+--convert-graft-file::
+ Creates graft commits for all entries in `$GIT_DIR/info/grafts`
+ and deletes that file upon success. The purpose is to help users
+ with transitioning off of the now-deprecated graft file.
-l <pattern>::
--list <pattern>::
its remote name.
-EXAMPLE
--------
+EXAMPLES
+--------
Imagine that you built your work on your `master` branch on top of
the `v1.0` release, and want it to be integrated to the project.
one of 'always', 'never', 'cc', 'compose', or 'auto'. See `--confirm`
in the previous section for the meaning of these values.
-EXAMPLE
--------
+EXAMPLES
+--------
Use gmail as the smtp server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To use 'git send-email' to send your patches through the GMail SMTP server,
The remote refs to update.
-Specifying the Refs
+SPECIFYING THE REFS
-------------------
There are three ways to specify which refs to update on the
If a `no-interactive-login` command exists, then it is run and the
interactive shell is aborted.
-EXAMPLE
--------
+EXAMPLES
+--------
To disable interactive logins, displaying a greeting instead:
The current branch is "master".
-EXAMPLE
--------
+EXAMPLES
+--------
If you keep your primary branches immediately under
`refs/heads`, and topic branches in subdirectories of
...
-----------------------------------------------------------------------------
-EXAMPLE
--------
+EXAMPLES
+--------
To show all references called "master", whether tags or heads or anything
else, and regardless of how deep in the reference naming hierarchy they are,
Concatenates the contents of said Makefiles in the head
of the branch `master`.
-Discussion
+DISCUSSION
----------
include::i18n.txt[]
Display or do not display detailed ahead/behind counts for the
branch relative to its upstream branch. Defaults to true.
+--renames::
+--no-renames::
+ Turn on/off rename detection regardless of user configuration.
+ See also linkgit:git-diff[1] `--no-renames`.
+
+--find-renames[=<n>]::
+ Turn on rename detection, optionally setting the similarity
+ threshold.
+ See also linkgit:git-diff[1] `--find-renames`.
+
<pathspec>...::
See the 'pathspec' entry in linkgit:gitglossary[7].
--quiet::
Only print error messages.
+--progress::
+ This option is only valid for add and update commands.
+ Progress status is reported on the standard error stream
+ by default when it is attached to a terminal, unless -q
+ is specified. This flag forces progress status even if the
+ standard error stream is not directed to a terminal.
+
--all::
This option is only valid for the deinit command. Unregister all
submodules in the working tree.
this option will be passed to the linkgit:git-clone[1] command.
+
*NOTE*: Do *not* use this option unless you have read the note
-for linkgit:git-clone[1]'s `--reference` and `--shared` options carefully.
+for linkgit:git-clone[1]'s `--reference`, `--shared`, and `--dissociate`
+options carefully.
+
+--dissociate::
+ This option is only valid for add and update commands. These
+ commands sometimes need to clone a remote repository. In this case,
+ this option will be passed to the linkgit:git-clone[1] command.
++
+*NOTE*: see the NOTE for the `--reference` option.
--recursive::
This option is only valid for foreach, update, status and sync commands.
config key: svn.useLogAuthor
--add-author-from::
- When committing to svn from Git (as part of 'commit-diff', 'set-tree' or 'dcommit'
+ When committing to svn from Git (as part of 'set-tree' or 'dcommit'
operations), if the existing log message doesn't already have a
`From:` or `Signed-off-by:` line, append a `From:` line based on the
Git commit's author string. If you use this, then `--use-log-author`
cleaner names.
The same applies to directories ending '/' and paths with '//'
-Using --refresh
+USING --REFRESH
---------------
`--refresh` does not calculate a new sha1 file or bring the index
up to date for mode/content changes. But what it *does* do is to
For example, you'd want to do this after doing a 'git read-tree', to link
up the stat index details with the proper files.
-Using --cacheinfo or --info-only
+USING --CACHEINFO OR --INFO-ONLY
--------------------------------
`--cacheinfo` is used to register a file that is not in the
current working directory. This is useful for minimum-checkout
object database.
-Using --index-info
+USING --INDEX-INFO
------------------
`--index-info` is a more powerful mechanism that lets you feed
------------
-Using ``assume unchanged'' bit
+USING ``ASSUME UNCHANGED'' BIT
------------------------------
Many operations in Git depend on your filesystem to have an
to mark them as "assume unchanged").
-Examples
+EXAMPLES
--------
To update and refresh only the files already checked out:
<9> now it checks with lstat(2) and finds it has been changed.
-Skip-worktree bit
+SKIP-WORKTREE BIT
-----------------
Skip-worktree bit can be defined in one (long) sentence: When reading
different from assume-unchanged bit's. Skip-worktree also takes
precedence over assume-unchanged bit when both are set.
-Split index
+SPLIT INDEX
-----------
This mode is designed for repositories with very large indexes, and
modification time is updated to the current time everytime a new split
index based on the shared index file is either created or read from.
-Untracked cache
+UNTRACKED CACHE
---------------
This cache is meant to speed up commands that involve determining
status" run with `core.untrackedCache=false` to flush out the leftover
bad data.
-File System Monitor
+FILE SYSTEM MONITOR
-------------------
This feature is intended to speed up git operations for repos that have
a command reads the index. When `--[no-]fsmonitor` are used, the file
system monitor is immediately added to or removed from the index.
-Configuration
+CONFIGURATION
-------------
The command honors `core.filemode` configuration variable. If
<ref> is updated or deleted atomically, a concurrent reader may
still see a subset of the modifications.
-Logging Updates
+LOGGING UPDATES
---------------
If config parameter "core.logAllRefUpdates" is true and the ref is one under
"refs/heads/", "refs/remotes/", "refs/notes/", or the symbolic ref HEAD; or
as well. (However, the configuration variables listing functionality
is deprecated in favor of `git config -l`.)
-EXAMPLE
+EXAMPLES
--------
$ git var GIT_AUTHOR_IDENT
Eric W. Biederman <ebiederm@lnxi.com> 1121223278 -0600
VARIABLES
-----------
+---------
GIT_AUTHOR_IDENT::
The author of a piece of code.
as a custom command and will use a shell eval to run the command with
the URLs passed as arguments.
-Note about konqueror
+NOTE ABOUT KONQUEROR
--------------------
When 'konqueror' is specified by a command-line option or a
------------
+
If `<commit-ish>` is omitted and neither `-b` nor `-B` nor `--detach` used,
-then, as a convenience, a new branch based at HEAD is created automatically,
-as if `-b $(basename <path>)` was specified.
+then, as a convenience, the new worktree is associated with a branch
+(call it `<branch>`) named after `$(basename <path>)`. If `<branch>`
+doesn't exist, a new branch based on HEAD is automatically created as
+if `-b <branch>` was given. If `<branch>` does exist, it will be
+checked out in the new worktree, if it's not checked out anywhere
+else, otherwise the command will refuse to create the worktree (unless
+`--force` is used).
list::
[verse]
'git' [--version] [--help] [-C <path>] [-c <name>=<value>]
[--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]
- [-p|--paginate|--no-pager] [--no-replace-objects] [--bare]
+ [-p|--paginate|-P|--no-pager] [--no-replace-objects] [--bare]
[--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]
[--super-prefix=<path>]
<command> [<args>]
configuration options (see the "Configuration Mechanism" section
below).
+-P::
--no-pager::
Do not pipe Git output into a pager.
------------
-EXAMPLE
--------
+EXAMPLES
+--------
If you have these three `gitattributes` file:
arguments, and stdin. See the documentation for each hook below for
details.
-'git init' may copy hooks to the new repository, depending on its
+`git init` may copy hooks to the new repository, depending on its
configuration. See the "TEMPLATE DIRECTORY" section in
linkgit:git-init[1] for details. When the rest of this document refers
to "default hooks" it's talking about the default template shipped
applypatch-msg
~~~~~~~~~~~~~~
-This hook is invoked by 'git am'. It takes a single
+This hook is invoked by linkgit:git-am[1]. It takes a single
parameter, the name of the file that holds the proposed commit
-log message. Exiting with a non-zero status causes 'git am' to abort
+log message. Exiting with a non-zero status causes `git am` to abort
before applying the patch.
The hook is allowed to edit the message file in place, and can
pre-applypatch
~~~~~~~~~~~~~~
-This hook is invoked by 'git am'. It takes no parameter, and is
+This hook is invoked by linkgit:git-am[1]. It takes no parameter, and is
invoked after the patch is applied, but before a commit is made.
If it exits with non-zero status, then the working tree will not be
post-applypatch
~~~~~~~~~~~~~~~
-This hook is invoked by 'git am'. It takes no parameter,
+This hook is invoked by linkgit:git-am[1]. It takes no parameter,
and is invoked after the patch is applied and a commit is made.
This hook is meant primarily for notification, and cannot affect
-the outcome of 'git am'.
+the outcome of `git am`.
pre-commit
~~~~~~~~~~
-This hook is invoked by 'git commit', and can be bypassed
+This hook is invoked by linkgit:git-commit[1], and can be bypassed
with the `--no-verify` option. It takes no parameters, and is
invoked before obtaining the proposed commit log message and
making a commit. Exiting with a non-zero status from this script
-causes the 'git commit' command to abort before creating a commit.
+causes the `git commit` command to abort before creating a commit.
The default 'pre-commit' hook, when enabled, catches introduction
of lines with trailing whitespaces and aborts the commit when
such a line is found.
-All the 'git commit' hooks are invoked with the environment
+All the `git commit` hooks are invoked with the environment
variable `GIT_EDITOR=:` if the command will not bring up an editor
to modify the commit message.
prepare-commit-msg
~~~~~~~~~~~~~~~~~~
-This hook is invoked by 'git commit' right after preparing the
+This hook is invoked by linkgit:git-commit[1] right after preparing the
default log message, and before the editor is started.
It takes one to three parameters. The first is the name of the file
(if a `.git/SQUASH_MSG` file exists); or `commit`, followed by
a commit SHA-1 (if a `-c`, `-C` or `--amend` option was given).
-If the exit status is non-zero, 'git commit' will abort.
+If the exit status is non-zero, `git commit` will abort.
The purpose of the hook is to edit the message file in place, and
it is not suppressed by the `--no-verify` option. A non-zero exit
commit-msg
~~~~~~~~~~
-This hook is invoked by 'git commit' and 'git merge', and can be
+This hook is invoked by linkgit:git-commit[1] and linkgit:git-merge[1], and can be
bypassed with the `--no-verify` option. It takes a single parameter,
the name of the file that holds the proposed commit log message.
Exiting with a non-zero status causes the command to abort.
post-commit
~~~~~~~~~~~
-This hook is invoked by 'git commit'. It takes no parameters, and is
+This hook is invoked by linkgit:git-commit[1]. It takes no parameters, and is
invoked after a commit is made.
This hook is meant primarily for notification, and cannot affect
-the outcome of 'git commit'.
+the outcome of `git commit`.
pre-rebase
~~~~~~~~~~
-This hook is called by 'git rebase' and can be used to prevent a
+This hook is called by linkgit:git-rebase[1] and can be used to prevent a
branch from getting rebased. The hook may be called with one or
two parameters. The first parameter is the upstream from which
the series was forked. The second parameter is the branch being
post-checkout
~~~~~~~~~~~~~
-This hook is invoked when a 'git checkout' is run after having updated the
+This hook is invoked when a linkgit:git-checkout[1] is run after having updated the
worktree. The hook is given three parameters: the ref of the previous HEAD,
the ref of the new HEAD (which may or may not have changed), and a flag
indicating whether the checkout was a branch checkout (changing branches,
flag=1) or a file checkout (retrieving a file from the index, flag=0).
-This hook cannot affect the outcome of 'git checkout'.
+This hook cannot affect the outcome of `git checkout`.
-It is also run after 'git clone', unless the --no-checkout (-n) option is
+It is also run after linkgit:git-clone[1], unless the `--no-checkout` (`-n`) option is
used. The first parameter given to the hook is the null-ref, the second the
-ref of the new HEAD and the flag is always 1. Likewise for 'git worktree add'
-unless --no-checkout is used.
+ref of the new HEAD and the flag is always 1. Likewise for `git worktree add`
+unless `--no-checkout` is used.
This hook can be used to perform repository validity checks, auto-display
differences from the previous HEAD if different, or set working dir metadata
post-merge
~~~~~~~~~~
-This hook is invoked by 'git merge', which happens when a 'git pull'
+This hook is invoked by linkgit:git-merge[1], which happens when a `git pull`
is done on a local repository. The hook takes a single parameter, a status
flag specifying whether or not the merge being done was a squash merge.
-This hook cannot affect the outcome of 'git merge' and is not executed,
+This hook cannot affect the outcome of `git merge` and is not executed,
if the merge failed due to conflicts.
This hook can be used in conjunction with a corresponding pre-commit hook to
pre-push
~~~~~~~~
-This hook is called by 'git push' and can be used to prevent a push from taking
-place. The hook is called with two parameters which provide the name and
-location of the destination remote, if a named remote is not being used both
-values will be the same.
+This hook is called by linkgit:git-push[1] and can be used to prevent
+a push from taking place. The hook is called with two parameters
+which provide the name and location of the destination remote, if a
+named remote is not being used both values will be the same.
Information about what is to be pushed is provided on the hook's standard
input with lines of the form:
than a name which could be expanded (such as `HEAD~`, or a SHA-1) it will be
supplied as it was originally given.
-If this hook exits with a non-zero status, 'git push' will abort without
+If this hook exits with a non-zero status, `git push` will abort without
pushing anything. Information about why the push is rejected may be sent
to the user by writing to standard error.
pre-receive
~~~~~~~~~~~
-This hook is invoked by 'git-receive-pack' when it reacts to
-'git push' and updates reference(s) in its repository.
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+`git push` and updates reference(s) in its repository.
Just before starting to update refs on the remote repository, the
pre-receive hook is invoked. Its exit status determines the success
or failure of the update.
still be prevented by the <<update,'update'>> hook.
Both standard output and standard error output are forwarded to
-'git send-pack' on the other end, so you can simply `echo` messages
+`git send-pack` on the other end, so you can simply `echo` messages
for the user.
The number of push options given on the command line of
update
~~~~~~
-This hook is invoked by 'git-receive-pack' when it reacts to
-'git push' and updates reference(s) in its repository.
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+`git push` and updates reference(s) in its repository.
Just before updating the ref on the remote repository, the update hook
is invoked. Its exit status determines the success or failure of
the ref update.
- and the new object name to be stored in the ref.
A zero exit from the update hook allows the ref to be updated.
-Exiting with a non-zero status prevents 'git-receive-pack'
+Exiting with a non-zero status prevents `git receive-pack`
from updating that ref.
This hook can be used to prevent 'forced' update on certain refs by
shell to restrict the user's access to only git commands.
Both standard output and standard error output are forwarded to
-'git send-pack' on the other end, so you can simply `echo` messages
+`git send-pack` on the other end, so you can simply `echo` messages
for the user.
The default 'update' hook, when enabled--and with
post-receive
~~~~~~~~~~~~
-This hook is invoked by 'git-receive-pack' when it reacts to
-'git push' and updates reference(s) in its repository.
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+`git push` and updates reference(s) in its repository.
It executes on the remote repository once after all the refs have
been updated.
<<pre-receive,'pre-receive'>>
hook does on its standard input.
-This hook does not affect the outcome of 'git-receive-pack', as it
+This hook does not affect the outcome of `git receive-pack`, as it
is called after the real work is done.
This supersedes the <<post-update,'post-update'>> hook in that it gets
names.
Both standard output and standard error output are forwarded to
-'git send-pack' on the other end, so you can simply `echo` messages
+`git send-pack` on the other end, so you can simply `echo` messages
for the user.
The default 'post-receive' hook is empty, but there is
post-update
~~~~~~~~~~~
-This hook is invoked by 'git-receive-pack' when it reacts to
-'git push' and updates reference(s) in its repository.
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+`git push` and updates reference(s) in its repository.
It executes on the remote repository once after all the refs have
been updated.
name of ref that was actually updated.
This hook is meant primarily for notification, and cannot affect
-the outcome of 'git-receive-pack'.
+the outcome of `git receive-pack`.
The 'post-update' hook can tell what are the heads that were pushed,
but it does not know what their original and updated values are,
them.
When enabled, the default 'post-update' hook runs
-'git update-server-info' to keep the information used by dumb
+`git update-server-info` to keep the information used by dumb
transports (e.g., HTTP) up to date. If you are publishing
a Git repository that is accessible via HTTP, you should
probably enable this hook.
Both standard output and standard error output are forwarded to
-'git send-pack' on the other end, so you can simply `echo` messages
+`git send-pack` on the other end, so you can simply `echo` messages
for the user.
push-to-checkout
~~~~~~~~~~~~~~~~
-This hook is invoked by 'git-receive-pack' when it reacts to
-'git push' and updates reference(s) in its repository, and when
+This hook is invoked by linkgit:git-receive-pack[1] when it reacts to
+`git push` and updates reference(s) in its repository, and when
the push tries to update the branch that is currently checked out
and the `receive.denyCurrentBranch` configuration variable is set to
`updateInstead`. Such a push by default is refused if the working
exit with a zero status.
For example, the hook can simply run `git read-tree -u -m HEAD "$1"`
-in order to emulate 'git fetch' that is run in the reverse direction
-with `git push`, as the two-tree form of `read-tree -u -m` is
+in order to emulate `git fetch` that is run in the reverse direction
+with `git push`, as the two-tree form of `git read-tree -u -m` is
essentially the same as `git checkout` that switches branches while
keeping the local changes in the working tree that do not interfere
with the difference between the branches.
pre-auto-gc
~~~~~~~~~~~
-This hook is invoked by 'git gc --auto'. It takes no parameter, and
-exiting with non-zero status from this script causes the 'git gc --auto'
-to abort.
+This hook is invoked by `git gc --auto` (see linkgit:git-gc[1]). It
+takes no parameter, and exiting with non-zero status from this script
+causes the `git gc --auto` to abort.
post-rewrite
~~~~~~~~~~~~
-This hook is invoked by commands that rewrite commits (`git commit
---amend`, 'git-rebase'; currently 'git-filter-branch' does 'not' call
+This hook is invoked by commands that rewrite commits
+(linkgit:git-commit[1] when called with `--amend` and
+linkgit:git-rebase[1]; currently `git filter-branch` does 'not' call
it!). Its first argument denotes the command it was invoked by:
currently one of `amend` or `rebase`. Further command-dependent
arguments may be passed in the future.
sendemail-validate
~~~~~~~~~~~~~~~~~~
-This hook is invoked by 'git send-email'. It takes a single parameter,
+This hook is invoked by linkgit:git-send-email[1]. It takes a single parameter,
the name of the file that holds the e-mail to be sent. Exiting with a
-non-zero status causes 'git send-email' to abort before sending any
+non-zero status causes `git send-email` to abort before sending any
e-mails.
fsmonitor-watchman
~~~~~~~~~~~~~~~~~~
-This hook is invoked when the configuration option core.fsmonitor is
-set to .git/hooks/fsmonitor-watchman. It takes two arguments, a version
+This hook is invoked when the configuration option `core.fsmonitor` is
+set to `.git/hooks/fsmonitor-watchman`. It takes two arguments, a version
(currently 1) and the time in elapsed nanoseconds since midnight,
January 1, 1970.
given.
An optimized way to tell git "all files have changed" is to return
-the filename '/'.
+the filename `/`.
The exit status determines whether git will use the data from the
hook to limit its search. On error, it will fall back to verifying
merge.renameLimit::
The number of files to consider when performing rename detection
during a merge; if not specified, defaults to the value of
- diff.renameLimit.
+ diff.renameLimit. This setting has no effect if rename detection
+ is turned off.
+
+merge.renames::
+ Whether and how Git detects renames. If set to "false",
+ rename detection is disabled. If set to "true", basic rename
+ detection is enabled. Defaults to the value of diff.renames.
merge.renormalize::
Tell Git that canonical representation of files in the
causing mismerges by tests done on actual merge commits
taken from Linux 2.6 kernel development history.
Additionally this can detect and handle merges involving
- renames. This is the default merge strategy when
- pulling or merging one branch.
+ renames, but currently cannot make use of detected
+ copies. This is the default merge strategy when pulling
+ or merging one branch.
+
The 'recursive' strategy can take the following options:
`merge.renormalize` configuration variable.
no-renames;;
- Turn off rename detection.
+ Turn off rename detection. This overrides the `merge.renames`
+ configuration variable.
See also linkgit:git-diff[1] `--no-renames`.
find-renames[=<n>];;
Turn on rename detection, optionally setting the similarity
- threshold. This is the default.
+ threshold. This is the default. This overrides the
+ 'merge.renames' configuration variable.
See also linkgit:git-diff[1] `--find-renames`.
rename-threshold=<n>;;
ones listed near the end of this list name trees and
blobs contained in a commit.
+NOTE: This document shows the "raw" syntax as seen by git. The shell
+and other UIs might require additional quoting to protect special
+characters and to avoid word splitting.
+
'<sha1>', e.g. 'dae86e1950b1277e545cee180551750029cfe735', 'dae86e'::
The full SHA-1 object name (40-byte hexadecimal string), or
a leading substring that is unique within the repository.
is matched. ':/!-foo' performs a negative match, while ':/!!foo' matches a
literal '!' character, followed by 'foo'. Any other sequence beginning with
':/!' is reserved for now.
+ Depending on the given text, the shell's word splitting rules might
+ require additional quoting.
'<rev>:<path>', e.g. 'HEAD:README', ':README', 'master:./README'::
A suffix ':' followed by a path names the blob or tree
with each step in the notation's expansion and selection carefully
spelt out:
+....
Args Expanded arguments Selected commits
D G H D
D F G H I J D F
= B ^B^1 ^B^2 ^B^3
= B ^D ^E ^F B
F^! D = F ^I ^J D G H D F
+....
repo-specific one; by overwriting, the higher-priority repo-specific
value is left at the end).
-The `git_config_with_options` function lets the caller examine config
+The `config_with_options` function lets the caller examine config
while adjusting some of the default behavior of `git_config`. It should
almost never be used by "regular" Git code that is looking up
configuration variables. It is intended for advanced callers like
`git-config`, which are intentionally tweaking the normal config-lookup
process. It takes two extra parameters:
-`filename`::
-If this parameter is non-NULL, it specifies the name of a file to
-parse for configuration, rather than looking in the usual files. Regular
-`git_config` defaults to `NULL`.
+`config_source`::
+If this parameter is non-NULL, it specifies the source to parse for
+configuration, rather than looking in the usual files. See `struct
+git_config_source` in `config.h` for details. Regular `git_config` defaults
+to `NULL`.
-`respect_includes`::
-Specify whether include directives should be followed in parsed files.
-Regular `git_config` defaults to `1`.
+`opts`::
+Specify options to adjust the behavior of parsing config files. See `struct
+config_options` in `config.h` for details. As an example: regular `git_config`
+sets `opts.respect_includes` to `1` by default.
Reading Specific Files
----------------------
Free all memory associated with the array and return it to the
initial, empty state.
+`oid_array_for_each`::
+ Iterate over each element of the list, executing the callback
+ function for each one. Does not sort the list, so any custom
+ hash order is retained. If the callback returns a non-zero
+ value, the iteration ends immediately and the callback's
+ return is propagated; otherwise, 0 is returned.
+
`oid_array_for_each_unique`::
- Efficiently iterate over each unique element of the list,
- executing the callback function for each one. If the array is
- not sorted, this function has the side effect of sorting it. If
- the callback returns a non-zero value, the iteration ends
- immediately and the callback's return is propagated; otherwise,
- 0 is returned.
+ Iterate over each unique element of the list in sorted order,
+ but otherwise behave like `oid_array_for_each`. If the array
+ is not sorted, this function has the side effect of sorting
+ it.
Examples
--------
- The trailer records 20-byte SHA-1 checksum of all of the above.
+=== Object types
+
+Valid object types are:
+
+- OBJ_COMMIT (1)
+- OBJ_TREE (2)
+- OBJ_BLOB (3)
+- OBJ_TAG (4)
+- OBJ_OFS_DELTA (6)
+- OBJ_REF_DELTA (7)
+
+Type 5 is reserved for future expansion. Type 0 is invalid.
+
+=== Deltified representation
+
+Conceptually there are only four object types: commit, tree, tag and
+blob. However to save space, an object could be stored as a "delta" of
+another "base" object. These representations are assigned new types
+ofs-delta and ref-delta, which is only valid in a pack file.
+
+Both ofs-delta and ref-delta store the "delta" to be applied to
+another object (called 'base object') to reconstruct the object. The
+difference between them is, ref-delta directly encodes 20-byte base
+object name. If the base object is in the same pack, ofs-delta encodes
+the offset of the base object in the pack instead.
+
+The base object could also be deltified if it's in the same pack.
+Ref-delta can also refer to an object outside the pack (i.e. the
+so-called "thin pack"). When stored on disk however, the pack should
+be self contained to avoid cyclic dependency.
+
+The delta data is a sequence of instructions to reconstruct an object
+from the base object. If the base object is deltified, it must be
+converted to canonical form first. Each instruction appends more and
+more data to the target object until it's complete. There are two
+supported instructions so far: one for copy a byte range from the
+source object and one for inserting new data embedded in the
+instruction itself.
+
+Each instruction has variable length. Instruction type is determined
+by the seventh bit of the first octet. The following diagrams follow
+the convention in RFC 1951 (Deflate compressed data format).
+
+==== Instruction to copy from base object
+
+ +----------+---------+---------+---------+---------+-------+-------+-------+
+ | 1xxxxxxx | offset1 | offset2 | offset3 | offset4 | size1 | size2 | size3 |
+ +----------+---------+---------+---------+---------+-------+-------+-------+
+
+This is the instruction format to copy a byte range from the source
+object. It encodes the offset to copy from and the number of bytes to
+copy. Offset and size are in little-endian order.
+
+All offset and size bytes are optional. This is to reduce the
+instruction size when encoding small offsets or sizes. The first seven
+bits in the first octet determines which of the next seven octets is
+present. If bit zero is set, offset1 is present. If bit one is set
+offset2 is present and so on.
+
+Note that a more compact instruction does not change offset and size
+encoding. For example, if only offset2 is omitted like below, offset3
+still contains bits 16-23. It does not become offset2 and contains
+bits 8-15 even if it's right next to offset1.
+
+ +----------+---------+---------+
+ | 10000101 | offset1 | offset3 |
+ +----------+---------+---------+
+
+In its most compact form, this instruction only takes up one byte
+(0x80) with both offset and size omitted, which will have default
+values zero. There is another exception: size zero is automatically
+converted to 0x10000.
+
+==== Instruction to add new data
+
+ +----------+============+
+ | 0xxxxxxx | data |
+ +----------+============+
+
+This is the instruction to construct target object without the base
+object. The following data is appended to the target object. The first
+seven bits of the first octet determines the size of data in
+bytes. The size must be non-zero.
+
+==== Reserved instruction
+
+ +----------+============
+ | 00000000 |
+ +----------+============
+
+This is the instruction reserved for future expansion.
+
== Original (version 1) pack-*.idx files have the following format:
- The header consists of 256 4-byte network byte order
Cannot be used with "deepen", but can be used with
"deepen-since".
+If the 'filter' feature is advertised, the following argument can be
+included in the client's request:
+
+ filter <filter-spec>
+ Request that various objects from the packfile be omitted
+ using one of several filtering techniques. These are intended
+ for use with partial clone and partial fetch operations. See
+ `rev-list` for possible "filter-spec" values.
+
The response of `fetch` is broken into a number of sections separated by
delimiter packets (0001), with each section beginning with its section
header.
1 - pack data
2 - progress messages
3 - fatal error message just before stream aborts
+
+ server-option
+~~~~~~~~~~~~~~~
+
+If advertised, indicates that any number of server specific options can be
+included in a request. This is done by sending each option as a
+"server-option=<option>" capability line in the capability-list section of
+a request.
+
+The provided options must not contain a NUL or LF character.
these commits have no parents.
*********************************************************
-The basic idea is to write the SHA-1s of shallow commits into
-$GIT_DIR/shallow, and handle its contents like the contents
-of $GIT_DIR/info/grafts (with the difference that shallow
-cannot contain parent information).
-
-This information is stored in a new file instead of grafts, or
-even the config, since the user should not touch that file
-at all (even throughout development of the shallow clone, it
-was never manually edited!).
+$GIT_DIR/shallow lists commit object names and tells Git to
+pretend as if they are root commits (e.g. "git log" traversal
+stops after showing them; "git fsck" does not complain saying
+the commits listed on their "parent" lines do not exist).
Each line contains exactly one SHA-1. When read, a commit_graft
will be constructed, which has nr_parent < 0 to make it easier
to discern from user provided grafts.
+Note that the shallow feature could not be changed easily to
+use replace refs: a commit containing a `mergetag` is not allowed
+to be replaced, not even by a root commit. Such a commit can be
+made shallow, though. Also, having a `shallow` file explicitly
+listing all the commits made shallow makes it a *lot* easier to
+do shallow-specific things such as to deepen the history.
+
Since fsck-objects relies on the library to read the objects,
it honours shallow commits automatically.
LIB_OBJS += refs/iterator.o
LIB_OBJS += refs/packed-backend.o
LIB_OBJS += refs/ref-cache.o
+LIB_OBJS += refspec.o
LIB_OBJS += ref-filter.o
LIB_OBJS += remote.o
LIB_OBJS += replace-object.o
int advice_add_embedded_repo = 1;
int advice_ignored_hook = 1;
int advice_waiting_for_editor = 1;
+int advice_graft_file_deprecated = 1;
static int advice_use_color = -1;
static char advice_colors[][COLOR_MAXLEN] = {
{ "addembeddedrepo", &advice_add_embedded_repo },
{ "ignoredhook", &advice_ignored_hook },
{ "waitingforeditor", &advice_waiting_for_editor },
+ { "graftfiledeprecated", &advice_graft_file_deprecated },
/* make this an alias for backward compatibility */
{ "pushnonfastforward", &advice_push_update_rejected }
extern int advice_add_embedded_repo;
extern int advice_ignored_hook;
extern int advice_waiting_for_editor;
+extern int advice_graft_file_deprecated;
int git_default_advice_config(const char *var, const char *value);
__attribute__((format (printf, 1, 2)))
if (postlen
? postlen < new_buf - postimage->buf
: postimage->len < new_buf - postimage->buf)
- die("BUG: caller miscounted postlen: asked %d, orig = %d, used = %d",
+ BUG("caller miscounted postlen: asked %d, orig = %d, used = %d",
(int)postlen, (int) postimage->len, (int)(new_buf - postimage->buf));
/* Fix the length of the whole thing */
unsigned mode = patch->new_mode;
if (!patch->is_new)
- die("BUG: patch to %s is not a creation", patch->old_name);
+ BUG("patch to %s is not a creation", patch->old_name);
pos = cache_name_pos(name, strlen(name));
if (pos < 0)
if (!patch->is_delete)
new_name = patch->new_name;
- if (old_name && !verify_path(old_name))
+ if (old_name && !verify_path(old_name, patch->old_mode))
return error(_("invalid path '%s'"), old_name);
- if (new_name && !verify_path(new_name))
+ if (new_name && !verify_path(new_name, patch->new_mode))
return error(_("invalid path '%s'"), new_name);
return 0;
}
{
struct patch *patch;
struct index_state result = { NULL };
- static struct lock_file lock;
+ struct lock_file lock = LOCK_INIT;
int res;
/* Once we start supporting the reverse patch, it may be
memcpy(header.name, path, pathlen);
if (S_ISREG(mode) && !args->convert &&
- oid_object_info(oid, &size) == OBJ_BLOB &&
+ oid_object_info(the_repository, oid, &size) == OBJ_BLOB &&
size > big_file_threshold)
buffer = NULL;
else if (S_ISLNK(mode) || S_ISREG(mode)) {
int r;
if (!ar->data)
- die("BUG: tar-filter archiver called with no filter defined");
+ BUG("tar-filter archiver called with no filter defined");
strbuf_addstr(&cmd, ar->data);
if (args->compression_level >= 0)
compressed_size = 0;
buffer = NULL;
} else if (S_ISREG(mode) || S_ISLNK(mode)) {
- enum object_type type = oid_object_info(oid, &size);
+ enum object_type type = oid_object_info(the_repository, oid,
+ &size);
method = 0;
attr2 = S_ISLNK(mode) ? ((mode | 0777) << 16) :
array->argc--;
}
+void argv_array_split(struct argv_array *array, const char *to_split)
+{
+ while (isspace(*to_split))
+ to_split++;
+ for (;;) {
+ const char *p = to_split;
+
+ if (!*p)
+ break;
+
+ while (*p && !isspace(*p))
+ p++;
+ argv_array_push_nodup(array, xstrndup(to_split, p - to_split));
+
+ while (isspace(*p))
+ p++;
+ to_split = p;
+ }
+}
+
void argv_array_clear(struct argv_array *array)
{
if (array->argv != empty_argv) {
void argv_array_pushl(struct argv_array *, ...);
void argv_array_pushv(struct argv_array *, const char **);
void argv_array_pop(struct argv_array *);
+/* Splits by whitespace; does not handle quoted arguments! */
+void argv_array_split(struct argv_array *, const char *);
void argv_array_clear(struct argv_array *);
const char **argv_array_detach(struct argv_array *);
size = hashmap_get_size(&map->map);
if (size < check->all_attrs_nr)
- die("BUG: interned attributes shouldn't be deleted");
+ BUG("interned attributes shouldn't be deleted");
/*
* If the number of attributes in the global dictionary has increased
break;
if (i >= check_vector.nr)
- die("BUG: no entry found");
+ BUG("no entry found");
/* shift entries over */
for (; i < check_vector.nr - 1; i++)
const struct git_attr *attr;
param = va_arg(params, const char *);
if (!param)
- die("BUG: counted %d != ended at %d",
+ BUG("counted %d != ended at %d",
check->nr, cnt);
attr = git_attr(param);
if (!attr)
- die("BUG: %s: not a valid attribute name", param);
+ BUG("%s: not a valid attribute name", param);
check->items[cnt].attr = attr;
}
va_end(params);
struct index_state *istate)
{
if (is_bare_repository() && new_direction != GIT_ATTR_INDEX)
- die("BUG: non-INDEX attr direction in a bare repo");
+ BUG("non-INDEX attr direction in a bare repo");
if (new_direction != direction)
drop_all_attr_stacks();
unsigned mode;
if (!get_tree_entry(commit_oid, path, &blob_oid, &mode) &&
- oid_object_info(&blob_oid, NULL) == OBJ_BLOB)
+ oid_object_info(the_repository, &blob_oid, NULL) == OBJ_BLOB)
return;
}
return 0;
if (get_tree_entry(&origin->commit->object.oid, origin->path, &origin->blob_oid, &origin->mode))
goto error_out;
- if (oid_object_info(&origin->blob_oid, NULL) != OBJ_BLOB)
+ if (oid_object_info(the_repository, &origin->blob_oid, NULL) != OBJ_BLOB)
goto error_out;
return 0;
error_out:
diff_setup_done(&diff_opts);
if (is_null_oid(&origin->commit->object.oid))
- do_diff_cache(&parent->tree->object.oid, &diff_opts);
+ do_diff_cache(get_commit_tree_oid(parent), &diff_opts);
else
- diff_tree_oid(&parent->tree->object.oid,
- &origin->commit->tree->object.oid,
+ diff_tree_oid(get_commit_tree_oid(parent),
+ get_commit_tree_oid(origin->commit),
"", &diff_opts);
diffcore_std(&diff_opts);
diff_setup_done(&diff_opts);
if (is_null_oid(&origin->commit->object.oid))
- do_diff_cache(&parent->tree->object.oid, &diff_opts);
+ do_diff_cache(get_commit_tree_oid(parent), &diff_opts);
else
- diff_tree_oid(&parent->tree->object.oid,
- &origin->commit->tree->object.oid,
+ diff_tree_oid(get_commit_tree_oid(parent),
+ get_commit_tree_oid(origin->commit),
"", &diff_opts);
diffcore_std(&diff_opts);
diff_opts.flags.find_copies_harder = 1;
if (is_null_oid(&target->commit->object.oid))
- do_diff_cache(&parent->tree->object.oid, &diff_opts);
+ do_diff_cache(get_commit_tree_oid(parent), &diff_opts);
else
- diff_tree_oid(&parent->tree->object.oid,
- &target->commit->tree->object.oid,
+ diff_tree_oid(get_commit_tree_oid(parent),
+ get_commit_tree_oid(target->commit),
"", &diff_opts);
if (!diff_opts.flags.find_copies_harder)
l->item = c;
if (add_decoration(&sb->revs->children,
&c->parents->item->object, l))
- die("BUG: not unique item in first-parent chain");
+ BUG("not unique item in first-parent chain");
c = c->parents->item;
}
#include "config.h"
#include "branch.h"
#include "refs.h"
+#include "refspec.h"
#include "remote.h"
#include "commit.h"
#include "worktree.h"
struct tracking {
- struct refspec spec;
+ struct refspec_item spec;
char *src;
const char *remote;
int matches;
static int check_tracking_branch(struct remote *remote, void *cb_data)
{
char *tracking_branch = cb_data;
- struct refspec query;
- memset(&query, 0, sizeof(struct refspec));
+ struct refspec_item query;
+ memset(&query, 0, sizeof(struct refspec_item));
query.dst = tracking_branch;
return !remote_find_tracking(remote, &query);
}
return 0;
}
-static struct lock_file lock_file;
-
static const char ignore_error[] =
N_("The following paths are ignored by one of your .gitignore files:\n");
int add_new_files;
int require_pathspec;
char *seen = NULL;
+ struct lock_file lock_file = LOCK_INIT;
git_config(add_config, NULL);
struct strbuf sb = STRBUF_INIT;
if (read_state_file(&sb, state, "next", 1) < 0)
- die("BUG: state file 'next' does not exist");
+ BUG("state file 'next' does not exist");
state->cur = strtol(sb.buf, NULL, 10);
if (read_state_file(&sb, state, "last", 1) < 0)
- die("BUG: state file 'last' does not exist");
+ BUG("state file 'last' does not exist");
state->last = strtol(sb.buf, NULL, 10);
if (read_author_script(state) < 0)
case PATCH_FORMAT_MBOXRD:
return split_mail_mbox(state, paths, keep_cr, 1);
default:
- die("BUG: invalid patch_format");
+ BUG("invalid patch_format");
}
return -1;
}
str = "b";
break;
default:
- die("BUG: invalid value for state->keep");
+ BUG("invalid value for state->keep");
}
write_state_text(state, "keep", str);
str = "t";
break;
default:
- die("BUG: invalid value for state->scissors");
+ BUG("invalid value for state->scissors");
}
write_state_text(state, "scissors", str);
mi.keep_non_patch_brackets_in_subject = 1;
break;
default:
- die("BUG: invalid value for state->keep");
+ BUG("invalid value for state->keep");
}
if (state->message_id)
mi.use_scissors = 1;
break;
default:
- die("BUG: invalid value for state->scissors");
+ BUG("invalid value for state->scissors");
}
mi.input = xfopen(mail, "r");
int options = 0;
if (init_apply_state(&apply_state, NULL))
- die("BUG: init_apply_state() failed");
+ BUG("init_apply_state() failed");
argv_array_push(&apply_opts, "apply");
argv_array_pushv(&apply_opts, state->git_apply_opts.argv);
apply_state.apply_verbosity = verbosity_silent;
if (check_apply_state(&apply_state, force_apply))
- die("BUG: check_apply_state() failed");
+ BUG("check_apply_state() failed");
argv_array_push(&apply_paths, am_path(state, "patch"));
char *their_tree_name;
if (get_oid("HEAD", &our_tree) < 0)
- hashcpy(our_tree.hash, EMPTY_TREE_SHA1_BIN);
+ oidcpy(&our_tree, the_hash_algo->empty_tree);
if (build_fake_ancestor(state, index_path))
return error("could not build fake ancestor");
am_rerere_clear();
if (get_oid("HEAD", &head))
- hashcpy(head.hash, EMPTY_TREE_SHA1_BIN);
+ oidcpy(&head, the_hash_algo->empty_tree);
if (clean_index(&head, &head))
die(_("failed to clean index"));
curr_branch = resolve_refdup("HEAD", 0, &curr_head, NULL);
has_curr_head = curr_branch && !is_null_oid(&curr_head);
if (!has_curr_head)
- hashcpy(curr_head.hash, EMPTY_TREE_SHA1_BIN);
+ oidcpy(&curr_head, the_hash_algo->empty_tree);
has_orig_head = !get_oid("ORIG_HEAD", &orig_head);
if (!has_orig_head)
- hashcpy(orig_head.hash, EMPTY_TREE_SHA1_BIN);
+ oidcpy(&orig_head, the_hash_algo->empty_tree);
clean_index(&curr_head, &orig_head);
ret = show_patch(&state);
break;
default:
- die("BUG: invalid resume value");
+ BUG("invalid resume value");
}
am_state_release(&state);
#include "cache.h"
#include "config.h"
+#include "color.h"
#include "builtin.h"
#include "commit.h"
#include "diff.h"
#include "dir.h"
#include "progress.h"
#include "blame.h"
+#include "string-list.h"
static char blame_usage[] = N_("git blame [<options>] [<rev-opts>] [<rev>] [--] <file>");
static int abbrev = -1;
static int no_whole_file_rename;
static int show_progress;
+static char repeated_meta_color[COLOR_MAXLEN];
+static int coloring_mode;
static struct date_mode blame_date_mode = { DATE_ISO8601 };
static size_t blame_date_width;
#define OUTPUT_PORCELAIN 010
#define OUTPUT_SHOW_NAME 020
#define OUTPUT_SHOW_NUMBER 040
-#define OUTPUT_SHOW_SCORE 0100
-#define OUTPUT_NO_AUTHOR 0200
+#define OUTPUT_SHOW_SCORE 0100
+#define OUTPUT_NO_AUTHOR 0200
#define OUTPUT_SHOW_EMAIL 0400
-#define OUTPUT_LINE_PORCELAIN 01000
+#define OUTPUT_LINE_PORCELAIN 01000
+#define OUTPUT_COLOR_LINE 02000
+#define OUTPUT_SHOW_AGE_WITH_COLOR 04000
static void emit_porcelain_details(struct blame_origin *suspect, int repeat)
{
putchar('\n');
}
+static struct color_field {
+ timestamp_t hop;
+ char col[COLOR_MAXLEN];
+} *colorfield;
+static int colorfield_nr, colorfield_alloc;
+
+static void parse_color_fields(const char *s)
+{
+ struct string_list l = STRING_LIST_INIT_DUP;
+ struct string_list_item *item;
+ enum { EXPECT_DATE, EXPECT_COLOR } next = EXPECT_COLOR;
+
+ colorfield_nr = 0;
+
+ /* Ideally this would be stripped and split at the same time? */
+ string_list_split(&l, s, ',', -1);
+ ALLOC_GROW(colorfield, colorfield_nr + 1, colorfield_alloc);
+
+ for_each_string_list_item(item, &l) {
+ switch (next) {
+ case EXPECT_DATE:
+ colorfield[colorfield_nr].hop = approxidate(item->string);
+ next = EXPECT_COLOR;
+ colorfield_nr++;
+ ALLOC_GROW(colorfield, colorfield_nr + 1, colorfield_alloc);
+ break;
+ case EXPECT_COLOR:
+ if (color_parse(item->string, colorfield[colorfield_nr].col))
+ die(_("expecting a color: %s"), item->string);
+ next = EXPECT_DATE;
+ break;
+ }
+ }
+
+ if (next == EXPECT_COLOR)
+ die (_("must end with a color"));
+
+ colorfield[colorfield_nr].hop = TIME_MAX;
+}
+
+static void setup_default_color_by_age(void)
+{
+ parse_color_fields("blue,12 month ago,white,1 month ago,red");
+}
+
+static void determine_line_heat(struct blame_entry *ent, const char **dest_color)
+{
+ int i = 0;
+ struct commit_info ci;
+ get_commit_info(ent->suspect->commit, &ci, 1);
+
+ while (i < colorfield_nr && ci.author_time > colorfield[i].hop)
+ i++;
+
+ *dest_color = colorfield[i].col;
+}
+
static void emit_other(struct blame_scoreboard *sb, struct blame_entry *ent, int opt)
{
int cnt;
struct commit_info ci;
char hex[GIT_MAX_HEXSZ + 1];
int show_raw_time = !!(opt & OUTPUT_RAW_TIMESTAMP);
+ const char *default_color = NULL, *color = NULL, *reset = NULL;
get_commit_info(suspect->commit, &ci, 1);
oid_to_hex_r(hex, &suspect->commit->object.oid);
cp = blame_nth_line(sb, ent->lno);
+
+ if (opt & OUTPUT_SHOW_AGE_WITH_COLOR) {
+ determine_line_heat(ent, &default_color);
+ color = default_color;
+ reset = GIT_COLOR_RESET;
+ }
+
for (cnt = 0; cnt < ent->num_lines; cnt++) {
char ch;
int length = (opt & OUTPUT_LONG_OBJECT_NAME) ? GIT_SHA1_HEXSZ : abbrev;
+ if (opt & OUTPUT_COLOR_LINE) {
+ if (cnt > 0) {
+ color = repeated_meta_color;
+ reset = GIT_COLOR_RESET;
+ } else {
+ color = default_color ? default_color : NULL;
+ reset = default_color ? GIT_COLOR_RESET : NULL;
+ }
+ }
+ if (color)
+ fputs(color, stdout);
+
if (suspect->commit->object.flags & UNINTERESTING) {
if (blank_boundary)
memset(hex, ' ', length);
printf(" %*d) ",
max_digits, ent->lno + 1 + cnt);
}
+ if (reset)
+ fputs(reset, stdout);
do {
ch = *cp++;
putchar(ch);
parse_date_format(value, &blame_date_mode);
return 0;
}
+ if (!strcmp(var, "color.blame.repeatedlines")) {
+ if (color_parse_mem(value, strlen(value), repeated_meta_color))
+ warning(_("invalid color '%s' in color.blame.repeatedLines"),
+ value);
+ return 0;
+ }
+ if (!strcmp(var, "color.blame.highlightrecent")) {
+ parse_color_fields(value);
+ return 0;
+ }
+
+ if (!strcmp(var, "blame.coloring")) {
+ if (!strcmp(value, "repeatedLines")) {
+ coloring_mode |= OUTPUT_COLOR_LINE;
+ } else if (!strcmp(value, "highlightRecent")) {
+ coloring_mode |= OUTPUT_SHOW_AGE_WITH_COLOR;
+ } else if (!strcmp(value, "none")) {
+ coloring_mode &= ~(OUTPUT_COLOR_LINE |
+ OUTPUT_SHOW_AGE_WITH_COLOR);
+ } else {
+ warning(_("invalid value for blame.coloring"));
+ return 0;
+ }
+ }
if (git_diff_heuristic_config(var, value, cb) < 0)
return -1;
if (get_oid(name, &oid))
return 0;
- return OBJ_NONE < oid_object_info(&oid, NULL);
+ return OBJ_NONE < oid_object_info(the_repository, &oid, NULL);
}
int cmd_blame(int argc, const char **argv, const char *prefix)
OPT_BIT('s', NULL, &output_option, N_("Suppress author name and timestamp (Default: off)"), OUTPUT_NO_AUTHOR),
OPT_BIT('e', "show-email", &output_option, N_("Show author email instead of name (Default: off)"), OUTPUT_SHOW_EMAIL),
OPT_BIT('w', NULL, &xdl_opts, N_("Ignore whitespace differences"), XDF_IGNORE_WHITESPACE),
+ OPT_BIT(0, "color-lines", &output_option, N_("color redundant metadata from previous line differently"), OUTPUT_COLOR_LINE),
+ OPT_BIT(0, "color-by-age", &output_option, N_("color lines by age"), OUTPUT_SHOW_AGE_WITH_COLOR),
/*
* The following two options are parsed by parse_revision_opt()
unsigned int range_i;
long anchor;
+ setup_default_color_by_age();
git_config(git_blame_config, &output_option);
init_revisions(&revs, NULL);
revs.date_mode = blame_date_mode;
blame_coalesce(&sb);
- if (!(output_option & OUTPUT_PORCELAIN))
+ if (!(output_option & (OUTPUT_COLOR_LINE | OUTPUT_SHOW_AGE_WITH_COLOR)))
+ output_option |= coloring_mode;
+
+ if (!(output_option & OUTPUT_PORCELAIN)) {
find_alignment(&sb, &output_option);
+ if (!*repeated_meta_color &&
+ (output_option & OUTPUT_COLOR_LINE))
+ strcpy(repeated_meta_color, GIT_COLOR_CYAN);
+ }
+ if (output_option & OUTPUT_ANNOTATE_COMPAT)
+ output_option &= ~(OUTPUT_COLOR_LINE | OUTPUT_SHOW_AGE_WITH_COLOR);
output(&sb, output_option);
free((void *)sb.final_buf);
if (!skip_prefix(oldref.buf, "refs/heads/", &interpreted_oldname) ||
!skip_prefix(newref.buf, "refs/heads/", &interpreted_newname)) {
- die("BUG: expected prefix missing for refs");
+ BUG("expected prefix missing for refs");
}
if (copy)
switch (opt) {
case 't':
oi.type_name = &sb;
- if (oid_object_info_extended(&oid, &oi, flags) < 0)
+ if (oid_object_info_extended(the_repository, &oid, &oi, flags) < 0)
die("git cat-file: could not get object info");
if (sb.len) {
printf("%s\n", sb.buf);
case 's':
oi.sizep = &size;
- if (oid_object_info_extended(&oid, &oi, flags) < 0)
+ if (oid_object_info_extended(the_repository, &oid, &oi, flags) < 0)
die("git cat-file: could not get object info");
printf("%lu\n", size);
return 0;
/* else fallthrough */
case 'p':
- type = oid_object_info(&oid, NULL);
+ type = oid_object_info(the_repository, &oid, NULL);
if (type < 0)
die("Not a valid object name %s", obj_name);
case 0:
if (type_from_string(exp_type) == OBJ_BLOB) {
struct object_id blob_oid;
- if (oid_object_info(&oid, NULL) == OBJ_TAG) {
+ if (oid_object_info(the_repository, &oid, NULL) == OBJ_TAG) {
char *buffer = read_object_file(&oid, &type,
&size);
const char *target;
} else
oidcpy(&blob_oid, &oid);
- if (oid_object_info(&blob_oid, NULL) == OBJ_BLOB)
+ if (oid_object_info(the_repository, &blob_oid, NULL) == OBJ_BLOB)
return stream_blob_to_fd(1, &blob_oid, NULL, 0);
/*
* we attempted to dereference a tag to a blob
die("could not convert '%s' %s",
oid_to_hex(oid), data->rest);
} else
- die("BUG: invalid cmdmode: %c", opt->cmdmode);
+ BUG("invalid cmdmode: %c", opt->cmdmode);
batch_write(opt, contents, size);
free(contents);
} else if (stream_blob_to_fd(1, oid, NULL, 0) < 0)
struct strbuf buf = STRBUF_INIT;
if (!data->skip_object_info &&
- oid_object_info_extended(&data->oid, &data->info,
+ oid_object_info_extended(the_repository, &data->oid, &data->info,
OBJECT_INFO_LOOKUP_REPLACE) < 0) {
printf("%s missing\n",
obj_name ? obj_name : oid_to_hex(&data->oid));
(uintmax_t)strlen(obj_name), obj_name);
break;
default:
- die("BUG: unknown get_sha1_with_context result %d\n",
+ BUG("unknown get_sha1_with_context result %d\n",
result);
break;
}
resolve_undo_clear();
if (opts->force) {
- ret = reset_tree(new_branch_info->commit->tree, opts, 1, writeout_error);
+ ret = reset_tree(get_commit_tree(new_branch_info->commit),
+ opts, 1, writeout_error);
if (ret)
return ret;
} else {
o.verbosity = 0;
work = write_tree_from_memory(&o);
- ret = reset_tree(new_branch_info->commit->tree, opts, 1,
+ ret = reset_tree(get_commit_tree(new_branch_info->commit),
+ opts, 1,
writeout_error);
if (ret)
return ret;
o.ancestor = old_branch_info->name;
o.branch1 = new_branch_info->name;
o.branch2 = "local";
- ret = merge_trees(&o, new_branch_info->commit->tree, work,
- old_branch_info->commit->tree, &result);
+ ret = merge_trees(&o,
+ get_commit_tree(new_branch_info->commit),
+ work,
+ get_commit_tree(old_branch_info->commit),
+ &result);
if (ret < 0)
exit(128);
- ret = reset_tree(new_branch_info->commit->tree, opts, 0,
+ ret = reset_tree(get_commit_tree(new_branch_info->commit),
+ opts, 0,
writeout_error);
strbuf_release(&o.obuf);
if (ret)
*source_tree = parse_tree_indirect(rev);
} else {
parse_commit_or_die(new_branch_info->commit);
- *source_tree = new_branch_info->commit->tree;
+ *source_tree = get_commit_tree(new_branch_info->commit);
}
if (!*source_tree) /* case (1): want a tree */
#include "parse-options.h"
#include "fetch-pack.h"
#include "refs.h"
+#include "refspec.h"
#include "tree.h"
#include "tree-walk.h"
#include "unpack-trees.h"
}
static struct ref *wanted_peer_refs(const struct ref *refs,
- struct refspec *refspec)
+ struct refspec_item *refspec)
{
struct ref *head = copy_ref(find_ref_by_name(refs, "HEAD"));
struct ref *local_refs = head;
} else if (remote_head_points_at) {
const char *head = remote_head_points_at->name;
if (!skip_prefix(head, "refs/heads/", &head))
- die("BUG: remote HEAD points at non-head?");
+ BUG("remote HEAD points at non-head?");
strbuf_addf(&value, "+%s:%s%s", remote_head_points_at->name,
branch_top->buf, head);
int err = 0, complete_refs_before_fetch = 1;
int submodule_progress;
- struct refspec *refspec;
- const char *fetch_pattern;
+ struct refspec_item refspec;
fetch_if_missing = 0;
if (option_required_reference.nr || option_optional_reference.nr)
setup_reference();
- fetch_pattern = value.buf;
- refspec = parse_fetch_refspec(1, &fetch_pattern);
+ refspec_item_init(&refspec, value.buf, REFSPEC_FETCH);
strbuf_reset(&value);
refs = transport_get_remote_refs(transport, NULL);
if (refs) {
- mapped_refs = wanted_peer_refs(refs, refspec);
+ mapped_refs = wanted_peer_refs(refs, &refspec);
/*
* transport_get_remote_refs() may return refs with null sha-1
* in mapped_refs (see struct transport->get_refs_list
strbuf_release(&value);
junk_mode = JUNK_LEAVE_ALL;
- free(refspec);
+ refspec_item_clear(&refspec);
return err;
}
git_config(column_config, NULL);
memset(&copts, 0, sizeof(copts));
- copts.width = term_columns();
copts.padding = 1;
argc = parse_options(argc, argv, "", options, builtin_column_usage, 0);
if (argc)
return 0;
}
+static int opt_parse_rename_score(const struct option *opt, const char *arg, int unset)
+{
+ const char **value = opt->value;
+ if (arg != NULL && *arg == '=')
+ arg = arg + 1;
+
+ *value = arg;
+ return 0;
+}
+
static void determine_whence(struct wt_status *s)
{
if (file_exists(git_path_merge_head()))
static void status_init_config(struct wt_status *s, config_fn_t fn)
{
wt_status_prepare(s);
+ init_diff_ui_defaults();
git_config(fn, s);
determine_whence(s);
- init_diff_ui_defaults();
s->hints = advice_status_hints; /* must come after git_config() */
}
static void assert_split_ident(struct ident_split *id, const struct strbuf *buf)
{
if (split_ident_line(id, buf->buf, buf->len) || !id->date_begin)
- die("BUG: unable to parse our own ident: %s", buf->buf);
+ BUG("unable to parse our own ident: %s", buf->buf);
}
static void export_one(const char *var, const char *s, const char *e, int hack)
return error(_("Invalid untracked files mode '%s'"), v);
return 0;
}
+ if (!strcmp(k, "diff.renamelimit")) {
+ if (s->rename_limit == -1)
+ s->rename_limit = git_config_int(k, v);
+ return 0;
+ }
+ if (!strcmp(k, "status.renamelimit")) {
+ s->rename_limit = git_config_int(k, v);
+ return 0;
+ }
+ if (!strcmp(k, "diff.renames")) {
+ if (s->detect_rename == -1)
+ s->detect_rename = git_config_rename(k, v);
+ return 0;
+ }
+ if (!strcmp(k, "status.renames")) {
+ s->detect_rename = git_config_rename(k, v);
+ return 0;
+ }
return git_diff_ui_config(k, v, NULL);
}
int cmd_status(int argc, const char **argv, const char *prefix)
{
+ static int no_renames = -1;
+ static const char *rename_score_arg = (const char *)-1;
static struct wt_status s;
int fd;
struct object_id oid;
N_("ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)"),
PARSE_OPT_OPTARG, NULL, (intptr_t)"all" },
OPT_COLUMN(0, "column", &s.colopts, N_("list untracked files in columns")),
+ OPT_BOOL(0, "no-renames", &no_renames, N_("do not detect renames")),
+ { OPTION_CALLBACK, 'M', "find-renames", &rename_score_arg,
+ N_("n"), N_("detect renames, optionally set similarity index"),
+ PARSE_OPT_OPTARG, opt_parse_rename_score },
OPT_END(),
};
s.ignore_submodule_arg = ignore_submodule_arg;
s.status_format = status_format;
s.verbose = verbose;
+ if (no_renames != -1)
+ s.detect_rename = !no_renames;
+ if ((intptr_t)rename_score_arg != -1) {
+ if (s.detect_rename < DIFF_DETECT_RENAME)
+ s.detect_rename = DIFF_DETECT_RENAME;
+ if (rename_score_arg)
+ s.rename_score = parse_rename_score(&rename_score_arg);
+ }
wt_status_collect(&s);
return xstrdup(value);
}
- die("BUG: cannot normalize type %d", type);
+ BUG("cannot normalize type %d", type);
}
static int get_color_found;
else {
loose_size += on_disk_bytes(st);
loose++;
- if (verbose && has_sha1_pack(oid->hash))
+ if (verbose && has_object_pack(oid))
packed_loose++;
}
return 0;
if (cmit)
describe_commit(&oid, &sb);
- else if (oid_object_info(&oid, NULL) == OBJ_BLOB)
+ else if (oid_object_info(the_repository, &oid, NULL) == OBJ_BLOB)
describe_blob(oid, &sb);
else
die(_("%s is neither a commit nor blob"), arg);
suffix = broken;
}
} else if (dirty) {
- static struct lock_file index_lock;
+ struct lock_file index_lock = LOCK_INIT;
struct rev_info revs;
struct argv_array args = ARGV_ARRAY_INIT;
int fd, result;
if (!obj)
die(_("invalid object '%s' given."), name);
if (obj->type == OBJ_COMMIT)
- obj = &((struct commit *)obj)->tree->object;
+ obj = &get_commit_tree(((struct commit *)obj))->object;
if (obj->type == OBJ_TREE) {
obj->flags |= flags;
continue;
if (!indices_loaded) {
- static struct lock_file lock;
+ struct lock_file lock = LOCK_INIT;
strbuf_reset(&buf);
strbuf_addf(&buf, "%s/wtindex", tmpdir);
if (hold_lock_file_for_update(&lock, buf.buf, 0) < 0 ||
#include "cache.h"
#include "config.h"
#include "refs.h"
+#include "refspec.h"
#include "commit.h"
#include "object.h"
#include "tag.h"
static int no_data;
static int full_tree;
static struct string_list extra_refs = STRING_LIST_INIT_NODUP;
-static struct refspec *refspecs;
-static int refspecs_nr;
+static struct refspec refspecs = REFSPEC_INIT_FETCH;
static int anonymize;
static int parse_opt_signed_tag_mode(const struct option *opt,
}
}
-/* Since intptr_t is C99, we do not use it here */
-static inline uint32_t *mark_to_ptr(uint32_t mark)
+static inline void *mark_to_ptr(uint32_t mark)
{
- return ((uint32_t *)NULL) + mark;
+ return (void *)(uintptr_t)mark;
}
static inline uint32_t ptr_to_mark(void * mark)
{
- return (uint32_t *)mark - (uint32_t *)NULL;
+ return (uint32_t)(uintptr_t)mark;
}
static inline void mark_object(struct object *object, uint32_t mark)
/* skip "committer", "author", "tagger", etc */
end_of_header = strchr(*beg, ' ');
if (!end_of_header)
- die("BUG: malformed line fed to anonymize_ident_line: %.*s",
+ BUG("malformed line fed to anonymize_ident_line: %.*s",
(int)(*end - *beg), *beg);
end_of_header++;
strbuf_add(out, *beg, end_of_header - *beg);
get_object_mark(&commit->parents->item->object) != 0 &&
!full_tree) {
parse_commit_or_die(commit->parents->item);
- diff_tree_oid(&commit->parents->item->tree->object.oid,
- &commit->tree->object.oid, "", &rev->diffopt);
+ diff_tree_oid(get_commit_tree_oid(commit->parents->item),
+ get_commit_tree_oid(commit), "", &rev->diffopt);
}
else
- diff_root_tree_oid(&commit->tree->object.oid,
+ diff_root_tree_oid(get_commit_tree_oid(commit),
"", &rev->diffopt);
/* Export the referenced blobs, and remember the marks. */
if (dwim_ref(e->name, strlen(e->name), &oid, &full_name) != 1)
continue;
- if (refspecs) {
+ if (refspecs.nr) {
char *private;
- private = apply_refspecs(refspecs, refspecs_nr, full_name);
+ private = apply_refspecs(&refspecs, full_name);
if (private) {
free(full_name);
full_name = private;
if (last_idnum < mark)
last_idnum = mark;
- type = oid_object_info(&oid, NULL);
+ type = oid_object_info(the_repository, &oid, NULL);
if (type < 0)
die("object not found: %s", oid_to_hex(&oid));
static void handle_deletes(void)
{
int i;
- for (i = 0; i < refspecs_nr; i++) {
- struct refspec *refspec = &refspecs[i];
+ for (i = 0; i < refspecs.nr; i++) {
+ struct refspec_item *refspec = &refspecs.items[i];
if (*refspec->src)
continue;
usage_with_options (fast_export_usage, options);
if (refspecs_list.nr) {
- const char **refspecs_str;
int i;
- ALLOC_ARRAY(refspecs_str, refspecs_list.nr);
for (i = 0; i < refspecs_list.nr; i++)
- refspecs_str[i] = refspecs_list.items[i].string;
-
- refspecs_nr = refspecs_list.nr;
- refspecs = parse_fetch_refspec(refspecs_nr, refspecs_str);
+ refspec_append(&refspecs, refspecs_list.items[i].string);
string_list_clear(&refspecs_list, 1);
- free(refspecs_str);
}
if (use_done_feature)
if (use_done_feature)
printf("done\n");
- free_refspec(refspecs_nr, refspecs);
+ refspec_clear(&refspecs);
return 0;
}
#include "config.h"
#include "repository.h"
#include "refs.h"
+#include "refspec.h"
#include "commit.h"
#include "builtin.h"
#include "string-list.h"
static int recurse_submodules = RECURSE_SUBMODULES_DEFAULT;
static int recurse_submodules_default = RECURSE_SUBMODULES_ON_DEMAND;
static int shown_url = 0;
-static int refmap_alloc, refmap_nr;
-static const char **refmap_array;
+static struct refspec refmap = REFSPEC_INIT_FETCH;
static struct list_objects_filter_options filter_options;
+static struct string_list server_options = STRING_LIST_INIT_DUP;
static int git_fetch_config(const char *k, const char *v, void *cb)
{
static int parse_refmap_arg(const struct option *opt, const char *arg, int unset)
{
- ALLOC_GROW(refmap_array, refmap_nr + 1, refmap_alloc);
-
/*
* "git fetch --refmap='' origin foo"
* can be used to tell the command not to store anywhere
*/
- if (*arg)
- refmap_array[refmap_nr++] = arg;
+ refspec_append(&refmap, arg);
+
return 0;
}
N_("accept refs that update .git/shallow")),
{ OPTION_CALLBACK, 0, "refmap", NULL, N_("refmap"),
N_("specify fetch refmap"), PARSE_OPT_NONEG, parse_refmap_arg },
+ OPT_STRING_LIST('o', "server-option", &server_options, N_("server-specific"), N_("option to transmit")),
OPT_SET_INT('4', "ipv4", &family, N_("use IPv4 addresses only"),
TRANSPORT_FAMILY_IPV4),
OPT_SET_INT('6', "ipv6", &family, N_("use IPv6 addresses only"),
for (i = 0; i < branch->merge_nr; i++) {
struct ref *rm, **old_tail = *tail;
- struct refspec refspec;
+ struct refspec_item refspec;
for (rm = *head; rm; rm = rm->next) {
if (branch_merge_matches(branch, i, rm->name)) {
}
static struct ref *get_ref_map(struct transport *transport,
- struct refspec *refspecs, int refspec_count,
+ struct refspec *rs,
int tags, int *autotags)
{
int i;
const struct ref *remote_refs;
- for (i = 0; i < refspec_count; i++) {
- if (!refspecs[i].exact_sha1) {
- const char *glob = strchr(refspecs[i].src, '*');
- if (glob)
- argv_array_pushf(&ref_prefixes, "%.*s",
- (int)(glob - refspecs[i].src),
- refspecs[i].src);
- else
- expand_ref_prefix(&ref_prefixes, refspecs[i].src);
- }
+ if (rs->nr)
+ refspec_ref_prefixes(rs, &ref_prefixes);
+ else if (transport->remote && transport->remote->fetch.nr)
+ refspec_ref_prefixes(&transport->remote->fetch, &ref_prefixes);
+
+ if (ref_prefixes.argc &&
+ (tags == TAGS_SET || (tags == TAGS_DEFAULT && !rs->nr))) {
+ argv_array_push(&ref_prefixes, "refs/tags/");
}
remote_refs = transport_get_remote_refs(transport, &ref_prefixes);
argv_array_clear(&ref_prefixes);
- if (refspec_count) {
+ if (rs->nr) {
struct refspec *fetch_refspec;
- int fetch_refspec_nr;
- for (i = 0; i < refspec_count; i++) {
- get_fetch_map(remote_refs, &refspecs[i], &tail, 0);
- if (refspecs[i].dst && refspecs[i].dst[0])
+ for (i = 0; i < rs->nr; i++) {
+ get_fetch_map(remote_refs, &rs->items[i], &tail, 0);
+ if (rs->items[i].dst && rs->items[i].dst[0])
*autotags = 1;
}
/* Merge everything on the command line (but not --tags) */
* by ref_remove_duplicates() in favor of one of these
* opportunistic entries with FETCH_HEAD_IGNORE.
*/
- if (refmap_array) {
- fetch_refspec = parse_fetch_refspec(refmap_nr, refmap_array);
- fetch_refspec_nr = refmap_nr;
- } else {
- fetch_refspec = transport->remote->fetch;
- fetch_refspec_nr = transport->remote->fetch_refspec_nr;
- }
+ if (refmap.nr)
+ fetch_refspec = &refmap;
+ else
+ fetch_refspec = &transport->remote->fetch;
- for (i = 0; i < fetch_refspec_nr; i++)
- get_fetch_map(ref_map, &fetch_refspec[i], &oref_tail, 1);
- } else if (refmap_array) {
+ for (i = 0; i < fetch_refspec->nr; i++)
+ get_fetch_map(ref_map, &fetch_refspec->items[i], &oref_tail, 1);
+ } else if (refmap.nr) {
die("--refmap option is only meaningful with command-line refspec(s).");
} else {
/* Use the defaults */
struct branch *branch = branch_get(NULL);
int has_merge = branch_has_merge_config(branch);
if (remote &&
- (remote->fetch_refspec_nr ||
+ (remote->fetch.nr ||
/* Note: has_merge implies non-NULL branch->remote_name */
(has_merge && !strcmp(branch->remote_name, remote->name)))) {
- for (i = 0; i < remote->fetch_refspec_nr; i++) {
- get_fetch_map(remote_refs, &remote->fetch[i], &tail, 0);
- if (remote->fetch[i].dst &&
- remote->fetch[i].dst[0])
+ for (i = 0; i < remote->fetch.nr; i++) {
+ get_fetch_map(remote_refs, &remote->fetch.items[i], &tail, 0);
+ if (remote->fetch.items[i].dst &&
+ remote->fetch.items[i].dst[0])
*autotags = 1;
if (!i && !has_merge && ref_map &&
- !remote->fetch[0].pattern)
+ !remote->fetch.items[0].pattern)
ref_map->fetch_head_status = FETCH_HEAD_MERGE;
}
/*
struct branch *current_branch = branch_get(NULL);
const char *pretty_ref = prettify_refname(ref->name);
- type = oid_object_info(&ref->new_oid, NULL);
+ type = oid_object_info(the_repository, &ref->new_oid, NULL);
if (type < 0)
die(_("object %s not found"), oid_to_hex(&ref->new_oid));
return ret;
}
-static int prune_refs(struct refspec *refs, int ref_count, struct ref *ref_map,
- const char *raw_url)
+static int prune_refs(struct refspec *rs, struct ref *ref_map,
+ const char *raw_url)
{
int url_len, i, result = 0;
- struct ref *ref, *stale_refs = get_stale_heads(refs, ref_count, ref_map);
+ struct ref *ref, *stale_refs = get_stale_heads(rs, ref_map);
char *url;
int summary_width = transport_summary_width(stale_refs);
const char *dangling_msg = dry_run
}
static int do_fetch(struct transport *transport,
- struct refspec *refs, int ref_count)
+ struct refspec *rs)
{
struct string_list existing_refs = STRING_LIST_INIT_DUP;
struct ref *ref_map;
goto cleanup;
}
- ref_map = get_ref_map(transport, refs, ref_count, tags, &autotags);
+ ref_map = get_ref_map(transport, rs, tags, &autotags);
if (!update_head_ok)
check_not_current_branch(ref_map);
* explicitly (via command line or configuration); we
* don't care whether --tags was specified.
*/
- if (ref_count) {
- prune_refs(refs, ref_count, ref_map, transport->url);
+ if (rs->nr) {
+ prune_refs(rs, ref_map, transport->url);
} else {
- prune_refs(transport->remote->fetch,
- transport->remote->fetch_refspec_nr,
+ prune_refs(&transport->remote->fetch,
ref_map,
transport->url);
}
static int fetch_one(struct remote *remote, int argc, const char **argv, int prune_tags_ok)
{
- static const char **refs = NULL;
- struct refspec *refspec;
- int ref_nr = 0;
- int j = 0;
+ struct refspec rs = REFSPEC_INIT_FETCH;
+ int i;
int exit_code;
int maybe_prune_tags;
int remote_via_config = remote_is_configured(remote, 0);
maybe_prune_tags = prune_tags_ok && prune_tags;
if (maybe_prune_tags && remote_via_config)
- add_prune_tags_to_fetch_refspec(remote);
-
- if (argc > 0 || (maybe_prune_tags && !remote_via_config)) {
- size_t nr_alloc = st_add3(argc, maybe_prune_tags, 1);
- refs = xcalloc(nr_alloc, sizeof(const char *));
- if (maybe_prune_tags) {
- refs[j++] = xstrdup("refs/tags/*:refs/tags/*");
- ref_nr++;
+ refspec_append(&remote->fetch, TAG_REFSPEC);
+
+ if (maybe_prune_tags && (argc || !remote_via_config))
+ refspec_append(&rs, TAG_REFSPEC);
+
+ for (i = 0; i < argc; i++) {
+ if (!strcmp(argv[i], "tag")) {
+ char *tag;
+ i++;
+ if (i >= argc)
+ die(_("You need to specify a tag name."));
+
+ tag = xstrfmt("refs/tags/%s:refs/tags/%s",
+ argv[i], argv[i]);
+ refspec_append(&rs, tag);
+ free(tag);
+ } else {
+ refspec_append(&rs, argv[i]);
}
}
- if (argc > 0) {
- int i;
- for (i = 0; i < argc; i++) {
- if (!strcmp(argv[i], "tag")) {
- i++;
- if (i >= argc)
- die(_("You need to specify a tag name."));
- refs[j++] = xstrfmt("refs/tags/%s:refs/tags/%s",
- argv[i], argv[i]);
- } else
- refs[j++] = argv[i];
- ref_nr++;
- }
- }
+ if (server_options.nr)
+ gtransport->server_options = &server_options;
sigchain_push_common(unlock_pack_on_signal);
atexit(unlock_pack);
- refspec = parse_fetch_refspec(ref_nr, refs);
- exit_code = do_fetch(gtransport, refspec, ref_nr);
- free_refspec(ref_nr, refspec);
+ exit_code = do_fetch(gtransport, &rs);
+ refspec_clear(&rs);
transport_disconnect(gtransport);
gtransport = NULL;
return exit_code;
const char *ret;
if (obj->type == OBJ_NONE) {
- enum object_type type = oid_object_info(&obj->oid, NULL);
+ enum object_type type = oid_object_info(the_repository,
+ &obj->oid, NULL);
if (type > 0)
object_as_type(obj, type, 0);
}
if (!(obj->flags & HAS_OBJ)) {
if (is_promisor_object(&obj->oid))
return;
- if (has_sha1_pack(obj->oid.hash))
+ if (has_object_pack(&obj->oid))
return; /* it is in pack - forget about it */
printf("missing %s %s\n", printable_type(obj),
describe_object(obj));
}
}
-static int fsck_obj(struct object *obj)
+static int fsck_obj(struct object *obj, void *buffer, unsigned long size)
{
int err;
if (fsck_walk(obj, NULL, &fsck_obj_options))
objerror(obj, "broken links");
- err = fsck_object(obj, NULL, 0, &fsck_obj_options);
+ err = fsck_object(obj, buffer, size, &fsck_obj_options);
if (err)
goto out;
}
obj->flags &= ~(REACHABLE | SEEN);
obj->flags |= HAS_OBJ;
- return fsck_obj(obj);
+ return fsck_obj(obj, buffer, size);
}
static int default_refs;
}
}
-static struct object *parse_loose_object(const struct object_id *oid,
- const char *path)
+static int fsck_loose(const struct object_id *oid, const char *path, void *data)
{
struct object *obj;
- void *contents;
enum object_type type;
unsigned long size;
+ void *contents;
int eaten;
- if (read_loose_object(path, oid, &type, &size, &contents) < 0)
- return NULL;
+ if (read_loose_object(path, oid, &type, &size, &contents) < 0) {
+ errors_found |= ERROR_OBJECT;
+ error("%s: object corrupt or missing: %s",
+ oid_to_hex(oid), path);
+ return 0; /* keep checking other objects */
+ }
if (!contents && type != OBJ_BLOB)
- die("BUG: read_loose_object streamed a non-blob");
+ BUG("read_loose_object streamed a non-blob");
obj = parse_object_buffer(oid, type, size, contents, &eaten);
-
- if (!eaten)
- free(contents);
- return obj;
-}
-
-static int fsck_loose(const struct object_id *oid, const char *path, void *data)
-{
- struct object *obj = parse_loose_object(oid, path);
-
if (!obj) {
errors_found |= ERROR_OBJECT;
- error("%s: object corrupt or missing: %s",
+ error("%s: object could not be parsed: %s",
oid_to_hex(oid), path);
+ if (!eaten)
+ free(contents);
return 0; /* keep checking other objects */
}
obj->flags &= ~(REACHABLE | SEEN);
obj->flags |= HAS_OBJ;
- if (fsck_obj(obj))
+ if (fsck_obj(obj, contents, size))
errors_found |= ERROR_OBJECT;
- return 0;
+
+ if (!eaten)
+ free(contents);
+ return 0; /* keep checking other objects, even if we saw an error */
}
static int fsck_cruft(const char *basename, const char *path, void *data)
}
stop_progress(&progress);
}
+
+ if (fsck_finish(&fsck_obj_options))
+ errors_found |= ERROR_OBJECT;
}
for (i = 0; i < argc; i++) {
#include "commit.h"
#include "packfile.h"
#include "object-store.h"
+#include "pack.h"
+#include "pack-objects.h"
+#include "blob.h"
+#include "tree.h"
#define FAILED_RUN "failed to run %s"
static const char *gc_log_expire = "1.day.ago";
static const char *prune_expire = "2.weeks.ago";
static const char *prune_worktrees_expire = "3.months.ago";
+static unsigned long big_pack_threshold;
+static unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE;
static struct argv_array pack_refs_cmd = ARGV_ARRAY_INIT;
static struct argv_array reflog = ARGV_ARRAY_INIT;
git_config_get_expiry("gc.worktreepruneexpire", &prune_worktrees_expire);
git_config_get_expiry("gc.logexpiry", &gc_log_expire);
+ git_config_get_ulong("gc.bigpackthreshold", &big_pack_threshold);
+ git_config_get_ulong("pack.deltacachesize", &max_delta_cache_size);
+
git_config(git_default_config, NULL);
}
return needed;
}
+static struct packed_git *find_base_packs(struct string_list *packs,
+ unsigned long limit)
+{
+ struct packed_git *p, *base = NULL;
+
+ for (p = get_packed_git(the_repository); p; p = p->next) {
+ if (!p->pack_local)
+ continue;
+ if (limit) {
+ if (p->pack_size >= limit)
+ string_list_append(packs, p->pack_name);
+ } else if (!base || base->pack_size < p->pack_size) {
+ base = p;
+ }
+ }
+
+ if (base)
+ string_list_append(packs, base->pack_name);
+
+ return base;
+}
+
static int too_many_packs(void)
{
struct packed_git *p;
return gc_auto_pack_limit < cnt;
}
-static void add_repack_all_option(void)
+static uint64_t total_ram(void)
+{
+#if defined(HAVE_SYSINFO)
+ struct sysinfo si;
+
+ if (!sysinfo(&si))
+ return si.totalram;
+#elif defined(HAVE_BSD_SYSCTL) && (defined(HW_MEMSIZE) || defined(HW_PHYSMEM))
+ int64_t physical_memory;
+ int mib[2];
+ size_t length;
+
+ mib[0] = CTL_HW;
+# if defined(HW_MEMSIZE)
+ mib[1] = HW_MEMSIZE;
+# else
+ mib[1] = HW_PHYSMEM;
+# endif
+ length = sizeof(int64_t);
+ if (!sysctl(mib, 2, &physical_memory, &length, NULL, 0))
+ return physical_memory;
+#elif defined(GIT_WINDOWS_NATIVE)
+ MEMORYSTATUSEX memInfo;
+
+ memInfo.dwLength = sizeof(MEMORYSTATUSEX);
+ if (GlobalMemoryStatusEx(&memInfo))
+ return memInfo.ullTotalPhys;
+#endif
+ return 0;
+}
+
+static uint64_t estimate_repack_memory(struct packed_git *pack)
+{
+ unsigned long nr_objects = approximate_object_count();
+ size_t os_cache, heap;
+
+ if (!pack || !nr_objects)
+ return 0;
+
+ /*
+ * First we have to scan through at least one pack.
+ * Assume enough room in OS file cache to keep the entire pack
+ * or we may accidentally evict data of other processes from
+ * the cache.
+ */
+ os_cache = pack->pack_size + pack->index_size;
+ /* then pack-objects needs lots more for book keeping */
+ heap = sizeof(struct object_entry) * nr_objects;
+ /*
+ * internal rev-list --all --objects takes up some memory too,
+ * let's say half of it is for blobs
+ */
+ heap += sizeof(struct blob) * nr_objects / 2;
+ /*
+ * and the other half is for trees (commits and tags are
+ * usually insignificant)
+ */
+ heap += sizeof(struct tree) * nr_objects / 2;
+ /* and then obj_hash[], underestimated in fact */
+ heap += sizeof(struct object *) * nr_objects;
+ /* revindex is used also */
+ heap += sizeof(struct revindex_entry) * nr_objects;
+ /*
+ * read_sha1_file() (either at delta calculation phase, or
+ * writing phase) also fills up the delta base cache
+ */
+ heap += delta_base_cache_limit;
+ /* and of course pack-objects has its own delta cache */
+ heap += max_delta_cache_size;
+
+ return os_cache + heap;
+}
+
+static int keep_one_pack(struct string_list_item *item, void *data)
+{
+ argv_array_pushf(&repack, "--keep-pack=%s", basename(item->string));
+ return 0;
+}
+
+static void add_repack_all_option(struct string_list *keep_pack)
{
if (prune_expire && !strcmp(prune_expire, "now"))
argv_array_push(&repack, "-a");
if (prune_expire)
argv_array_pushf(&repack, "--unpack-unreachable=%s", prune_expire);
}
+
+ if (keep_pack)
+ for_each_string_list(keep_pack, keep_one_pack, NULL);
}
static void add_repack_incremental_option(void)
* we run "repack -A -d -l". Otherwise we tell the caller
* there is no need.
*/
- if (too_many_packs())
- add_repack_all_option();
- else if (too_many_loose_objects())
+ if (too_many_packs()) {
+ struct string_list keep_pack = STRING_LIST_INIT_NODUP;
+
+ if (big_pack_threshold) {
+ find_base_packs(&keep_pack, big_pack_threshold);
+ if (keep_pack.nr >= gc_auto_pack_limit) {
+ big_pack_threshold = 0;
+ string_list_clear(&keep_pack, 0);
+ find_base_packs(&keep_pack, 0);
+ }
+ } else {
+ struct packed_git *p = find_base_packs(&keep_pack, 0);
+ uint64_t mem_have, mem_want;
+
+ mem_have = total_ram();
+ mem_want = estimate_repack_memory(p);
+
+ /*
+ * Only allow 1/2 of memory for pack-objects, leave
+ * the rest for the OS and other processes in the
+ * system.
+ */
+ if (!mem_have || mem_want < mem_have / 2)
+ string_list_clear(&keep_pack, 0);
+ }
+
+ add_repack_all_option(&keep_pack);
+ string_list_clear(&keep_pack, 0);
+ } else if (too_many_loose_objects())
add_repack_incremental_option();
else
return 0;
/* return NULL on success, else hostname running the gc */
static const char *lock_repo_for_gc(int force, pid_t* ret_pid)
{
- static struct lock_file lock;
+ struct lock_file lock = LOCK_INIT;
char my_host[HOST_NAME_MAX + 1];
struct strbuf sb = STRBUF_INIT;
struct stat st;
const char *name;
pid_t pid;
int daemonized = 0;
+ int keep_base_pack = -1;
timestamp_t dummy;
struct option builtin_gc_options[] = {
OPT_BOOL_F(0, "force", &force,
N_("force running gc even if there may be another gc running"),
PARSE_OPT_NOCOMPLETE),
+ OPT_BOOL(0, "keep-largest-pack", &keep_base_pack,
+ N_("repack all other packs except the largest pack")),
OPT_END()
};
*/
daemonized = !daemonize();
}
- } else
- add_repack_all_option();
+ } else {
+ struct string_list keep_pack = STRING_LIST_INIT_NODUP;
+
+ if (keep_base_pack != -1) {
+ if (keep_base_pack)
+ find_base_packs(&keep_pack, 0);
+ } else if (big_pack_threshold) {
+ find_base_packs(&keep_pack, big_pack_threshold);
+ }
+
+ add_repack_all_option(&keep_pack);
+ string_list_clear(&keep_pack, 0);
+ }
name = lock_repo_for_gc(force, &pid);
if (name) {
strbuf_addstr(&name, repo->submodule_prefix);
}
- repo_read_index(repo);
+ if (repo_read_index(repo) < 0)
+ die("index file corrupt");
for (nr = 0; nr < repo->index->cache_nr; nr++) {
const struct cache_entry *ce = repo->index->cache[nr];
if (!(obj->flags & FLAG_CHECKED)) {
unsigned long size;
- int type = oid_object_info(&obj->oid, &size);
+ int type = oid_object_info(the_repository, &obj->oid, &size);
if (type <= 0)
die(_("did not receive expected object %s"),
oid_to_hex(&obj->oid));
enum object_type has_type;
unsigned long has_size;
read_lock();
- has_type = oid_object_info(oid, &has_size);
+ has_type = oid_object_info(the_repository, oid, &has_size);
if (has_type < 0)
die(_("cannot read existing object info %s"), oid_to_hex(oid));
if (has_type != type || has_size != size)
blob->object.flags |= FLAG_CHECKED;
else
die(_("invalid blob object %s"), oid_to_hex(oid));
+ if (do_fsck_object &&
+ fsck_object(&blob->object, (void *)data, size, &fsck_options))
+ die(_("fsck error in packed object"));
} else {
struct object *obj;
int eaten;
die(_("invalid %s"), type_name(type));
if (do_fsck_object &&
fsck_object(obj, buf, size, &fsck_options))
- die(_("Error in object"));
+ die(_("fsck error in packed object"));
if (strict && fsck_walk(obj, NULL, &fsck_options))
die(_("Not all child objects of %s are reachable"), oid_to_hex(&obj->oid));
if (obj->type == OBJ_COMMIT) {
struct commit *commit = (struct commit *) obj;
if (detach_commit_buffer(commit, NULL) != data)
- die("BUG: parse_object_buffer transmogrified our buffer");
+ BUG("parse_object_buffer transmogrified our buffer");
}
obj->flags |= FLAG_CHECKED;
}
if (!compare_and_swap_type(&child->real_type, OBJ_REF_DELTA,
base->obj->real_type))
- die("BUG: child->real_type != OBJ_REF_DELTA");
+ BUG("child->real_type != OBJ_REF_DELTA");
resolve_delta(child, base, result);
if (base->ref_first == base->ref_last && base->ofs_last == -1)
} else
chmod(final_index_name, 0444);
+ if (do_fsck_object)
+ add_packed_git(final_index_name, strlen(final_index_name), 0);
+
if (!from_stdin) {
printf("%s\n", sha1_to_hex(hash));
} else {
{
const uint32_t *idx1, *idx2;
uint32_t i;
+ const uint32_t hashwords = the_hash_algo->rawsz / sizeof(uint32_t);
/* The address of the 4-byte offset table */
idx1 = (((const uint32_t *)p->index_data)
+ 2 /* 8-byte header */
+ 256 /* fan out */
- + 5 * p->num_objects /* 20-byte SHA-1 table */
+ + hashwords * p->num_objects /* object ID table */
+ p->num_objects /* CRC32 table */
);
pack_hash);
else
close(input_fd);
+
+ if (do_fsck_object && fsck_finish(&fsck_options))
+ die(_("fsck error in pack objects"));
+
free(objects);
strbuf_release(&index_name_buf);
if (pack_name == NULL)
else if (get_shared_repository() == PERM_EVERYBODY)
xsnprintf(buf, sizeof(buf), "%d", OLD_PERM_EVERYBODY);
else
- die("BUG: invalid value for shared_repository");
+ BUG("invalid value for shared_repository");
git_config_set("core.sharedrepository", buf);
git_config_set("receive.denyNonFastforwards", "true");
}
open_next_file(NULL, rev->numbered_files ? NULL : "cover-letter", rev, quiet))
return;
- log_write_email_headers(rev, head, &pp.after_subject, &need_8bit_cte);
+ log_write_email_headers(rev, head, &pp.after_subject, &need_8bit_cte, 0);
for (i = 0; !need_8bit_cte && i < nr; i++) {
const char *buf = get_commit_buffer(list[i], NULL);
diff_setup_done(&opts);
- diff_tree_oid(&origin->tree->object.oid,
- &head->tree->object.oid,
+ diff_tree_oid(get_commit_tree_oid(origin),
+ get_commit_tree_oid(head),
"", &opts);
diffcore_std(&opts);
diff_flush(&opts);
*/
pos = index_name_pos(istate, ent->name, ent->len);
if (0 <= pos)
- die("BUG: killed-file %.*s not found",
+ BUG("killed-file %.*s not found",
ent->len, ent->name);
pos = -pos - 1;
while (pos < istate->cache_nr &&
const char **pattern = NULL;
struct argv_array ref_prefixes = ARGV_ARRAY_INIT;
int i;
+ struct string_list server_options = STRING_LIST_INIT_DUP;
struct remote *remote;
struct transport *transport;
2, PARSE_OPT_NOCOMPLETE),
OPT_BOOL(0, "symref", &show_symref_target,
N_("show underlying ref in addition to the object pointed by it")),
+ OPT_STRING_LIST('o', "server-option", &server_options, N_("server-specific"), N_("option to transmit")),
OPT_END()
};
transport = transport_get(remote, NULL);
if (uploadpack != NULL)
transport_set_option(transport, TRANS_OPT_UPLOADPACK, uploadpack);
+ if (server_options.nr)
+ transport->server_options = &server_options;
ref = transport_get_remote_refs(transport, &ref_prefixes);
if (transport_disconnect(transport)) {
char size_text[24];
if (!strcmp(type, blob_type)) {
unsigned long size;
- if (oid_object_info(oid, &size) == OBJ_BAD)
+ if (oid_object_info(the_repository, oid, &size) == OBJ_BAD)
xsnprintf(size_text, sizeof(size_text),
"BAD");
else
#include "run-command.h"
#include "diff.h"
#include "refs.h"
+#include "refspec.h"
#include "commit.h"
#include "diffcore.h"
#include "revision.h"
return rc;
}
-static void read_empty(unsigned const char *sha1, int verbose)
+static void read_empty(const struct object_id *oid, int verbose)
{
int i = 0;
const char *args[7];
args[i++] = "-v";
args[i++] = "-m";
args[i++] = "-u";
- args[i++] = EMPTY_TREE_SHA1_HEX;
- args[i++] = sha1_to_hex(sha1);
+ args[i++] = empty_tree_oid_hex();
+ args[i++] = oid_to_hex(oid);
args[i] = NULL;
if (run_command_v_opt(args, RUN_GIT_CMD))
die(_("read-tree failed"));
}
-static void reset_hard(unsigned const char *sha1, int verbose)
+static void reset_hard(const struct object_id *oid, int verbose)
{
int i = 0;
const char *args[6];
args[i++] = "-v";
args[i++] = "--reset";
args[i++] = "-u";
- args[i++] = sha1_to_hex(sha1);
+ args[i++] = oid_to_hex(oid);
args[i] = NULL;
if (run_command_v_opt(args, RUN_GIT_CMD))
if (is_null_oid(stash))
return;
- reset_hard(head->hash, 1);
+ reset_hard(head, 1);
args[2] = oid_to_hex(stash);
struct commit_list *remoteheads,
struct commit *head)
{
- static struct lock_file lock;
+ struct lock_file lock = LOCK_INIT;
const char *head_arg = "HEAD";
hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
{
struct object_id result_tree, result_commit;
struct commit_list *parents, **pptr = &parents;
- static struct lock_file lock;
+ struct lock_file lock = LOCK_INIT;
hold_locked_index(&lock, LOCK_DIE_ON_ERROR);
refresh_cache(REFRESH_QUIET);
if (remoteheads->next)
die(_("Can merge only exactly one commit into empty head"));
remote_head_oid = &remoteheads->item->object.oid;
- read_empty(remote_head_oid->hash, 0);
+ read_empty(remote_head_oid, 0);
update_ref("initial pull", "HEAD", remote_head_oid, NULL, 0,
UPDATE_REFS_DIE_ON_ERR);
goto done;
}
/* Check the type of object identified by sha1 */
- obj_type = oid_object_info(&oid, NULL);
+ obj_type = oid_object_info(the_repository, &oid, NULL);
if (obj_type < 0) {
if (allow_missing) {
; /* no problem - missing objects are presumed to be of the right type */
return path;
}
-static struct lock_file lock_file;
#define SUBMODULE_WITH_GITDIR ((const char *)1)
static void prepare_move_submodule(const char *src, int first,
enum update_mode { BOTH = 0, WORKING_DIRECTORY, INDEX } *modes;
struct stat st;
struct string_list src_for_dst = STRING_LIST_INIT_NODUP;
+ struct lock_file lock_file = LOCK_INIT;
git_config(git_default_config, NULL);
if (d.buf.len || allow_empty) {
write_note_data(&d, &new_note);
if (add_note(t, &object, &new_note, combine_notes_overwrite))
- die("BUG: combine_notes_overwrite failed");
+ BUG("combine_notes_overwrite failed");
commit_notes(t, "Notes added by 'git notes add'");
} else {
fprintf(stderr, _("Removing note for object %s\n"),
}
if (add_note(t, &object, from_note, combine_notes_overwrite))
- die("BUG: combine_notes_overwrite failed");
+ BUG("combine_notes_overwrite failed");
commit_notes(t, "Notes added by 'git notes copy'");
out:
free_notes(t);
if (d.buf.len || allow_empty) {
write_note_data(&d, &new_note);
if (add_note(t, &object, &new_note, combine_notes_overwrite))
- die("BUG: combine_notes_overwrite failed");
+ BUG("combine_notes_overwrite failed");
logmsg = xstrfmt("Notes added by 'git notes %s'", argv[0]);
} else {
fprintf(stderr, _("Removing note for object %s\n"),
const char *short_ref = NULL;
if (!skip_prefix(o.local_ref, "refs/notes/", &short_ref))
- die("BUG: local ref %s is outside of refs/notes/",
+ BUG("local ref %s is outside of refs/notes/",
o.local_ref);
strbuf_addf(&merge_key, "notes.%s.mergeStrategy", short_ref);
#include "list.h"
#include "packfile.h"
#include "object-store.h"
+#include "dir.h"
+
+#define IN_PACK(obj) oe_in_pack(&to_pack, obj)
+#define SIZE(obj) oe_size(&to_pack, obj)
+#define SET_SIZE(obj,size) oe_set_size(&to_pack, obj, size)
+#define DELTA_SIZE(obj) oe_delta_size(&to_pack, obj)
+#define DELTA(obj) oe_delta(&to_pack, obj)
+#define DELTA_CHILD(obj) oe_delta_child(&to_pack, obj)
+#define DELTA_SIBLING(obj) oe_delta_sibling(&to_pack, obj)
+#define SET_DELTA(obj, val) oe_set_delta(&to_pack, obj, val)
+#define SET_DELTA_SIZE(obj, val) oe_set_delta_size(&to_pack, obj, val)
+#define SET_DELTA_CHILD(obj, val) oe_set_delta_child(&to_pack, obj, val)
+#define SET_DELTA_SIBLING(obj, val) oe_set_delta_sibling(&to_pack, obj, val)
static const char *pack_usage[] = {
N_("git pack-objects --stdout [<options>...] [< <ref-list> | < <object-list>]"),
static struct packing_data to_pack;
static struct pack_idx_entry **written_list;
-static uint32_t nr_result, nr_written;
+static uint32_t nr_result, nr_written, nr_seen;
static int non_empty;
static int reuse_delta = 1, reuse_object = 1;
static int local;
static int have_non_local_packs;
static int incremental;
-static int ignore_packed_keep;
+static int ignore_packed_keep_on_disk;
+static int ignore_packed_keep_in_core;
static int allow_ofs_delta;
static struct pack_idx_option pack_idx_opts;
static const char *base_name;
static int exclude_promisor_objects;
static unsigned long delta_cache_size = 0;
-static unsigned long max_delta_cache_size = 256 * 1024 * 1024;
+static unsigned long max_delta_cache_size = DEFAULT_DELTA_CACHE_SIZE;
static unsigned long cache_max_small_delta_size = 1000;
static unsigned long window_memory_limit = 0;
buf = read_object_file(&entry->idx.oid, &type, &size);
if (!buf)
die("unable to read %s", oid_to_hex(&entry->idx.oid));
- base_buf = read_object_file(&entry->delta->idx.oid, &type, &base_size);
+ base_buf = read_object_file(&DELTA(entry)->idx.oid, &type,
+ &base_size);
if (!base_buf)
die("unable to read %s",
- oid_to_hex(&entry->delta->idx.oid));
+ oid_to_hex(&DELTA(entry)->idx.oid));
delta_buf = diff_delta(base_buf, base_size,
buf, size, &delta_size, 0);
- if (!delta_buf || delta_size != entry->delta_size)
+ if (!delta_buf || delta_size != DELTA_SIZE(entry))
die("delta size changed");
free(buf);
free(base_buf);
enum object_type type;
void *buf;
struct git_istream *st = NULL;
+ const unsigned hashsz = the_hash_algo->rawsz;
if (!usable_delta) {
- if (entry->type == OBJ_BLOB &&
- entry->size > big_file_threshold &&
+ if (oe_type(entry) == OBJ_BLOB &&
+ oe_size_greater_than(&to_pack, entry, big_file_threshold) &&
(st = open_istream(&entry->idx.oid, &type, &size, NULL)) != NULL)
buf = NULL;
else {
FREE_AND_NULL(entry->delta_data);
entry->z_delta_size = 0;
} else if (entry->delta_data) {
- size = entry->delta_size;
+ size = DELTA_SIZE(entry);
buf = entry->delta_data;
entry->delta_data = NULL;
- type = (allow_ofs_delta && entry->delta->idx.offset) ?
+ type = (allow_ofs_delta && DELTA(entry)->idx.offset) ?
OBJ_OFS_DELTA : OBJ_REF_DELTA;
} else {
buf = get_delta(entry);
- size = entry->delta_size;
- type = (allow_ofs_delta && entry->delta->idx.offset) ?
+ size = DELTA_SIZE(entry);
+ type = (allow_ofs_delta && DELTA(entry)->idx.offset) ?
OBJ_OFS_DELTA : OBJ_REF_DELTA;
}
* encoding of the relative offset for the delta
* base from this object's position in the pack.
*/
- off_t ofs = entry->idx.offset - entry->delta->idx.offset;
+ off_t ofs = entry->idx.offset - DELTA(entry)->idx.offset;
unsigned pos = sizeof(dheader) - 1;
dheader[pos] = ofs & 127;
while (ofs >>= 7)
dheader[--pos] = 128 | (--ofs & 127);
- if (limit && hdrlen + sizeof(dheader) - pos + datalen + 20 >= limit) {
+ if (limit && hdrlen + sizeof(dheader) - pos + datalen + hashsz >= limit) {
if (st)
close_istream(st);
free(buf);
} else if (type == OBJ_REF_DELTA) {
/*
* Deltas with a base reference contain
- * an additional 20 bytes for the base sha1.
+ * additional bytes for the base object ID.
*/
- if (limit && hdrlen + 20 + datalen + 20 >= limit) {
+ if (limit && hdrlen + hashsz + datalen + hashsz >= limit) {
if (st)
close_istream(st);
free(buf);
return 0;
}
hashwrite(f, header, hdrlen);
- hashwrite(f, entry->delta->idx.oid.hash, 20);
- hdrlen += 20;
+ hashwrite(f, DELTA(entry)->idx.oid.hash, hashsz);
+ hdrlen += hashsz;
} else {
- if (limit && hdrlen + datalen + 20 >= limit) {
+ if (limit && hdrlen + datalen + hashsz >= limit) {
if (st)
close_istream(st);
free(buf);
static off_t write_reuse_object(struct hashfile *f, struct object_entry *entry,
unsigned long limit, int usable_delta)
{
- struct packed_git *p = entry->in_pack;
+ struct packed_git *p = IN_PACK(entry);
struct pack_window *w_curs = NULL;
struct revindex_entry *revidx;
off_t offset;
- enum object_type type = entry->type;
+ enum object_type type = oe_type(entry);
off_t datalen;
unsigned char header[MAX_PACK_OBJECT_HEADER],
dheader[MAX_PACK_OBJECT_HEADER];
unsigned hdrlen;
+ const unsigned hashsz = the_hash_algo->rawsz;
+ unsigned long entry_size = SIZE(entry);
- if (entry->delta)
- type = (allow_ofs_delta && entry->delta->idx.offset) ?
+ if (DELTA(entry))
+ type = (allow_ofs_delta && DELTA(entry)->idx.offset) ?
OBJ_OFS_DELTA : OBJ_REF_DELTA;
hdrlen = encode_in_pack_object_header(header, sizeof(header),
- type, entry->size);
+ type, entry_size);
offset = entry->in_pack_offset;
revidx = find_pack_revindex(p, offset);
datalen -= entry->in_pack_header_size;
if (!pack_to_stdout && p->index_version == 1 &&
- check_pack_inflate(p, &w_curs, offset, datalen, entry->size)) {
+ check_pack_inflate(p, &w_curs, offset, datalen, entry_size)) {
error("corrupt packed object for %s",
oid_to_hex(&entry->idx.oid));
unuse_pack(&w_curs);
}
if (type == OBJ_OFS_DELTA) {
- off_t ofs = entry->idx.offset - entry->delta->idx.offset;
+ off_t ofs = entry->idx.offset - DELTA(entry)->idx.offset;
unsigned pos = sizeof(dheader) - 1;
dheader[pos] = ofs & 127;
while (ofs >>= 7)
dheader[--pos] = 128 | (--ofs & 127);
- if (limit && hdrlen + sizeof(dheader) - pos + datalen + 20 >= limit) {
+ if (limit && hdrlen + sizeof(dheader) - pos + datalen + hashsz >= limit) {
unuse_pack(&w_curs);
return 0;
}
hdrlen += sizeof(dheader) - pos;
reused_delta++;
} else if (type == OBJ_REF_DELTA) {
- if (limit && hdrlen + 20 + datalen + 20 >= limit) {
+ if (limit && hdrlen + hashsz + datalen + hashsz >= limit) {
unuse_pack(&w_curs);
return 0;
}
hashwrite(f, header, hdrlen);
- hashwrite(f, entry->delta->idx.oid.hash, 20);
- hdrlen += 20;
+ hashwrite(f, DELTA(entry)->idx.oid.hash, hashsz);
+ hdrlen += hashsz;
reused_delta++;
} else {
- if (limit && hdrlen + datalen + 20 >= limit) {
+ if (limit && hdrlen + datalen + hashsz >= limit) {
unuse_pack(&w_curs);
return 0;
}
else
limit = pack_size_limit - write_offset;
- if (!entry->delta)
+ if (!DELTA(entry))
usable_delta = 0; /* no delta */
else if (!pack_size_limit)
usable_delta = 1; /* unlimited packfile */
- else if (entry->delta->idx.offset == (off_t)-1)
+ else if (DELTA(entry)->idx.offset == (off_t)-1)
usable_delta = 0; /* base was written to another pack */
- else if (entry->delta->idx.offset)
+ else if (DELTA(entry)->idx.offset)
usable_delta = 1; /* base already exists in this pack */
else
usable_delta = 0; /* base could end up in another pack */
if (!reuse_object)
to_reuse = 0; /* explicit */
- else if (!entry->in_pack)
+ else if (!IN_PACK(entry))
to_reuse = 0; /* can't reuse what we don't have */
- else if (entry->type == OBJ_REF_DELTA || entry->type == OBJ_OFS_DELTA)
+ else if (oe_type(entry) == OBJ_REF_DELTA ||
+ oe_type(entry) == OBJ_OFS_DELTA)
/* check_object() decided it for us ... */
to_reuse = usable_delta;
/* ... but pack split may override that */
- else if (entry->type != entry->in_pack_type)
+ else if (oe_type(entry) != entry->in_pack_type)
to_reuse = 0; /* pack has delta which is unusable */
- else if (entry->delta)
+ else if (DELTA(entry))
to_reuse = 0; /* we want to pack afresh */
else
to_reuse = 1; /* we have it in-pack undeltified,
}
/* if we are deltified, write out base object first. */
- if (e->delta) {
+ if (DELTA(e)) {
e->idx.offset = 1; /* now recurse */
- switch (write_one(f, e->delta, offset)) {
+ switch (write_one(f, DELTA(e), offset)) {
case WRITE_ONE_RECURSIVE:
/* we cannot depend on this one */
- e->delta = NULL;
+ SET_DELTA(e, NULL);
break;
default:
break;
/* add this node... */
add_to_write_order(wo, endp, e);
/* all its siblings... */
- for (s = e->delta_sibling; s; s = s->delta_sibling) {
+ for (s = DELTA_SIBLING(e); s; s = DELTA_SIBLING(s)) {
add_to_write_order(wo, endp, s);
}
}
/* drop down a level to add left subtree nodes if possible */
- if (e->delta_child) {
+ if (DELTA_CHILD(e)) {
add_to_order = 1;
- e = e->delta_child;
+ e = DELTA_CHILD(e);
} else {
add_to_order = 0;
/* our sibling might have some children, it is next */
- if (e->delta_sibling) {
- e = e->delta_sibling;
+ if (DELTA_SIBLING(e)) {
+ e = DELTA_SIBLING(e);
continue;
}
/* go back to our parent node */
- e = e->delta;
- while (e && !e->delta_sibling) {
+ e = DELTA(e);
+ while (e && !DELTA_SIBLING(e)) {
/* we're on the right side of a subtree, keep
* going up until we can go right again */
- e = e->delta;
+ e = DELTA(e);
}
if (!e) {
/* done- we hit our original root node */
return;
}
/* pass it off to sibling at this level */
- e = e->delta_sibling;
+ e = DELTA_SIBLING(e);
}
};
}
{
struct object_entry *root;
- for (root = e; root->delta; root = root->delta)
+ for (root = e; DELTA(root); root = DELTA(root))
; /* nothing */
add_descendants_to_write_order(wo, endp, root);
}
for (i = 0; i < to_pack.nr_objects; i++) {
objects[i].tagged = 0;
objects[i].filled = 0;
- objects[i].delta_child = NULL;
- objects[i].delta_sibling = NULL;
+ SET_DELTA_CHILD(&objects[i], NULL);
+ SET_DELTA_SIBLING(&objects[i], NULL);
}
/*
*/
for (i = to_pack.nr_objects; i > 0;) {
struct object_entry *e = &objects[--i];
- if (!e->delta)
+ if (!DELTA(e))
continue;
/* Mark me as the first child */
- e->delta_sibling = e->delta->delta_child;
- e->delta->delta_child = e;
+ e->delta_sibling_idx = DELTA(e)->delta_child_idx;
+ SET_DELTA_CHILD(DELTA(e), e);
}
/*
* And then all remaining commits and tags.
*/
for (i = last_untagged; i < to_pack.nr_objects; i++) {
- if (objects[i].type != OBJ_COMMIT &&
- objects[i].type != OBJ_TAG)
+ if (oe_type(&objects[i]) != OBJ_COMMIT &&
+ oe_type(&objects[i]) != OBJ_TAG)
continue;
add_to_write_order(wo, &wo_end, &objects[i]);
}
* And then all the trees.
*/
for (i = last_untagged; i < to_pack.nr_objects; i++) {
- if (objects[i].type != OBJ_TREE)
+ if (oe_type(&objects[i]) != OBJ_TREE)
continue;
add_to_write_order(wo, &wo_end, &objects[i]);
}
die_errno("unable to seek in reused packfile");
if (reuse_packfile_offset < 0)
- reuse_packfile_offset = reuse_packfile->pack_size - 20;
+ reuse_packfile_offset = reuse_packfile->pack_size - the_hash_algo->rawsz;
total = to_write = reuse_packfile_offset - sizeof(struct pack_header);
if (write_bitmap_index) {
bitmap_writer_set_checksum(oid.hash);
- bitmap_writer_build_type_index(written_list, nr_written);
+ bitmap_writer_build_type_index(
+ &to_pack, written_list, nr_written);
}
finish_tmp_packfile(&tmpname, pack_tmp_name,
* Otherwise, we signal "-1" at the end to tell the caller that we do
* not know either way, and it needs to check more packs.
*/
- if (!ignore_packed_keep &&
+ if (!ignore_packed_keep_on_disk &&
+ !ignore_packed_keep_in_core &&
(!local || !have_non_local_packs))
return 1;
if (local && !p->pack_local)
return 0;
- if (ignore_packed_keep && p->pack_local && p->pack_keep)
+ if (p->pack_local &&
+ ((ignore_packed_keep_on_disk && p->pack_keep) ||
+ (ignore_packed_keep_in_core && p->pack_keep_in_core)))
return 0;
/* we don't know yet; keep looking for more packs */
int want;
struct list_head *pos;
- if (!exclude && local && has_loose_object_nonlocal(oid->hash))
+ if (!exclude && local && has_loose_object_nonlocal(oid))
return 0;
/*
entry = packlist_alloc(&to_pack, oid->hash, index_pos);
entry->hash = hash;
- if (type)
- entry->type = type;
+ oe_set_type(entry, type);
if (exclude)
entry->preferred_base = 1;
else
nr_result++;
if (found_pack) {
- entry->in_pack = found_pack;
+ oe_set_in_pack(&to_pack, entry, found_pack);
entry->in_pack_offset = found_offset;
}
off_t found_offset = 0;
uint32_t index_pos;
+ display_progress(progress_state, ++nr_seen);
+
if (have_duplicate_entry(oid, exclude, &index_pos))
return 0;
create_object_entry(oid, type, pack_name_hash(name),
exclude, name && no_try_delta(name),
index_pos, found_pack, found_offset);
-
- display_progress(progress_state, nr_result);
return 1;
}
{
uint32_t index_pos;
+ display_progress(progress_state, ++nr_seen);
+
if (have_duplicate_entry(oid, 0, &index_pos))
return 0;
return 0;
create_object_entry(oid, type, name_hash, 0, 0, index_pos, pack, offset);
-
- display_progress(progress_state, nr_result);
return 1;
}
static void check_object(struct object_entry *entry)
{
- if (entry->in_pack) {
- struct packed_git *p = entry->in_pack;
+ unsigned long canonical_size;
+
+ if (IN_PACK(entry)) {
+ struct packed_git *p = IN_PACK(entry);
struct pack_window *w_curs = NULL;
const unsigned char *base_ref = NULL;
struct object_entry *base_entry;
unsigned long avail;
off_t ofs;
unsigned char *buf, c;
+ enum object_type type;
+ unsigned long in_pack_size;
buf = use_pack(p, &w_curs, entry->in_pack_offset, &avail);
* since non-delta representations could still be reused.
*/
used = unpack_object_header_buffer(buf, avail,
- &entry->in_pack_type,
- &entry->size);
+ &type,
+ &in_pack_size);
if (used == 0)
goto give_up;
+ if (type < 0)
+ BUG("invalid type %d", type);
+ entry->in_pack_type = type;
+
/*
* Determine if this is a delta and if so whether we can
* reuse it or not. Otherwise let's find out as cheaply as
switch (entry->in_pack_type) {
default:
/* Not a delta hence we've already got all we need. */
- entry->type = entry->in_pack_type;
+ oe_set_type(entry, entry->in_pack_type);
+ SET_SIZE(entry, in_pack_size);
entry->in_pack_header_size = used;
- if (entry->type < OBJ_COMMIT || entry->type > OBJ_BLOB)
+ if (oe_type(entry) < OBJ_COMMIT || oe_type(entry) > OBJ_BLOB)
goto give_up;
unuse_pack(&w_curs);
return;
if (reuse_delta && !entry->preferred_base)
base_ref = use_pack(p, &w_curs,
entry->in_pack_offset + used, NULL);
- entry->in_pack_header_size = used + 20;
+ entry->in_pack_header_size = used + the_hash_algo->rawsz;
break;
case OBJ_OFS_DELTA:
buf = use_pack(p, &w_curs,
* deltify other objects against, in order to avoid
* circular deltas.
*/
- entry->type = entry->in_pack_type;
- entry->delta = base_entry;
- entry->delta_size = entry->size;
- entry->delta_sibling = base_entry->delta_child;
- base_entry->delta_child = entry;
+ oe_set_type(entry, entry->in_pack_type);
+ SET_SIZE(entry, in_pack_size); /* delta size */
+ SET_DELTA(entry, base_entry);
+ SET_DELTA_SIZE(entry, in_pack_size);
+ entry->delta_sibling_idx = base_entry->delta_child_idx;
+ SET_DELTA_CHILD(base_entry, entry);
unuse_pack(&w_curs);
return;
}
- if (entry->type) {
+ if (oe_type(entry)) {
+ off_t delta_pos;
+
/*
* This must be a delta and we already know what the
* final object type is. Let's extract the actual
* object size from the delta header.
*/
- entry->size = get_size_from_delta(p, &w_curs,
- entry->in_pack_offset + entry->in_pack_header_size);
- if (entry->size == 0)
+ delta_pos = entry->in_pack_offset + entry->in_pack_header_size;
+ canonical_size = get_size_from_delta(p, &w_curs, delta_pos);
+ if (canonical_size == 0)
goto give_up;
+ SET_SIZE(entry, canonical_size);
unuse_pack(&w_curs);
return;
}
unuse_pack(&w_curs);
}
- entry->type = oid_object_info(&entry->idx.oid, &entry->size);
- /*
- * The error condition is checked in prepare_pack(). This is
- * to permit a missing preferred base object to be ignored
- * as a preferred base. Doing so can result in a larger
- * pack file, but the transfer will still take place.
- */
+ oe_set_type(entry,
+ oid_object_info(the_repository, &entry->idx.oid, &canonical_size));
+ if (entry->type_valid) {
+ SET_SIZE(entry, canonical_size);
+ } else {
+ /*
+ * Bad object type is checked in prepare_pack(). This is
+ * to permit a missing preferred base object to be ignored
+ * as a preferred base. Doing so can result in a larger
+ * pack file, but the transfer will still take place.
+ */
+ }
}
static int pack_offset_sort(const void *_a, const void *_b)
{
const struct object_entry *a = *(struct object_entry **)_a;
const struct object_entry *b = *(struct object_entry **)_b;
+ const struct packed_git *a_in_pack = IN_PACK(a);
+ const struct packed_git *b_in_pack = IN_PACK(b);
/* avoid filesystem trashing with loose objects */
- if (!a->in_pack && !b->in_pack)
+ if (!a_in_pack && !b_in_pack)
return oidcmp(&a->idx.oid, &b->idx.oid);
- if (a->in_pack < b->in_pack)
+ if (a_in_pack < b_in_pack)
return -1;
- if (a->in_pack > b->in_pack)
+ if (a_in_pack > b_in_pack)
return 1;
return a->in_pack_offset < b->in_pack_offset ? -1 :
(a->in_pack_offset > b->in_pack_offset);
*/
static void drop_reused_delta(struct object_entry *entry)
{
- struct object_entry **p = &entry->delta->delta_child;
+ unsigned *idx = &to_pack.objects[entry->delta_idx - 1].delta_child_idx;
struct object_info oi = OBJECT_INFO_INIT;
+ enum object_type type;
+ unsigned long size;
+
+ while (*idx) {
+ struct object_entry *oe = &to_pack.objects[*idx - 1];
- while (*p) {
- if (*p == entry)
- *p = (*p)->delta_sibling;
+ if (oe == entry)
+ *idx = oe->delta_sibling_idx;
else
- p = &(*p)->delta_sibling;
+ idx = &oe->delta_sibling_idx;
}
- entry->delta = NULL;
+ SET_DELTA(entry, NULL);
entry->depth = 0;
- oi.sizep = &entry->size;
- oi.typep = &entry->type;
- if (packed_object_info(entry->in_pack, entry->in_pack_offset, &oi) < 0) {
+ oi.sizep = &size;
+ oi.typep = &type;
+ if (packed_object_info(the_repository, IN_PACK(entry), entry->in_pack_offset, &oi) < 0) {
/*
* We failed to get the info from this pack for some reason;
* fall back to sha1_object_info, which may find another copy.
- * And if that fails, the error will be recorded in entry->type
+ * And if that fails, the error will be recorded in oe_type(entry)
* and dealt with in prepare_pack().
*/
- entry->type = oid_object_info(&entry->idx.oid, &entry->size);
+ oe_set_type(entry,
+ oid_object_info(the_repository, &entry->idx.oid, &size));
+ } else {
+ oe_set_type(entry, type);
}
+ SET_SIZE(entry, size);
}
/*
for (cur = entry, total_depth = 0;
cur;
- cur = cur->delta, total_depth++) {
+ cur = DELTA(cur), total_depth++) {
if (cur->dfs_state == DFS_DONE) {
/*
* We've already seen this object and know it isn't
* is a bug.
*/
if (cur->dfs_state != DFS_NONE)
- die("BUG: confusing delta dfs state in first pass: %d",
+ BUG("confusing delta dfs state in first pass: %d",
cur->dfs_state);
/*
* it's not a delta, we're done traversing, but we'll mark it
* done to save time on future traversals.
*/
- if (!cur->delta) {
+ if (!DELTA(cur)) {
cur->dfs_state = DFS_DONE;
break;
}
* We keep all commits in the chain that we examined.
*/
cur->dfs_state = DFS_ACTIVE;
- if (cur->delta->dfs_state == DFS_ACTIVE) {
+ if (DELTA(cur)->dfs_state == DFS_ACTIVE) {
drop_reused_delta(cur);
cur->dfs_state = DFS_DONE;
break;
* an extra "next" pointer to keep going after we reset cur->delta.
*/
for (cur = entry; cur; cur = next) {
- next = cur->delta;
+ next = DELTA(cur);
/*
* We should have a chain of zero or more ACTIVE states down to
if (cur->dfs_state == DFS_DONE)
break;
else if (cur->dfs_state != DFS_ACTIVE)
- die("BUG: confusing delta dfs state in second pass: %d",
+ BUG("confusing delta dfs state in second pass: %d",
cur->dfs_state);
/*
uint32_t i;
struct object_entry **sorted_by_offset;
+ if (progress)
+ progress_state = start_progress(_("Counting objects"),
+ to_pack.nr_objects);
+
sorted_by_offset = xcalloc(to_pack.nr_objects, sizeof(struct object_entry *));
for (i = 0; i < to_pack.nr_objects; i++)
sorted_by_offset[i] = to_pack.objects + i;
for (i = 0; i < to_pack.nr_objects; i++) {
struct object_entry *entry = sorted_by_offset[i];
check_object(entry);
- if (big_file_threshold < entry->size)
+ if (entry->type_valid &&
+ oe_size_greater_than(&to_pack, entry, big_file_threshold))
entry->no_try_delta = 1;
+ display_progress(progress_state, i + 1);
}
+ stop_progress(&progress_state);
/*
* This must happen in a second pass, since we rely on the delta
{
const struct object_entry *a = *(struct object_entry **)_a;
const struct object_entry *b = *(struct object_entry **)_b;
+ enum object_type a_type = oe_type(a);
+ enum object_type b_type = oe_type(b);
+ unsigned long a_size = SIZE(a);
+ unsigned long b_size = SIZE(b);
- if (a->type > b->type)
+ if (a_type > b_type)
return -1;
- if (a->type < b->type)
+ if (a_type < b_type)
return 1;
if (a->hash > b->hash)
return -1;
return -1;
if (a->preferred_base < b->preferred_base)
return 1;
- if (a->size > b->size)
+ if (a_size > b_size)
return -1;
- if (a->size < b->size)
+ if (a_size < b_size)
return 1;
return a < b ? -1 : (a > b); /* newest first */
}
#endif
+/*
+ * Return the size of the object without doing any delta
+ * reconstruction (so non-deltas are true object sizes, but deltas
+ * return the size of the delta data).
+ */
+unsigned long oe_get_size_slow(struct packing_data *pack,
+ const struct object_entry *e)
+{
+ struct packed_git *p;
+ struct pack_window *w_curs;
+ unsigned char *buf;
+ enum object_type type;
+ unsigned long used, avail, size;
+
+ if (e->type_ != OBJ_OFS_DELTA && e->type_ != OBJ_REF_DELTA) {
+ read_lock();
+ if (oid_object_info(the_repository, &e->idx.oid, &size) < 0)
+ die(_("unable to get size of %s"),
+ oid_to_hex(&e->idx.oid));
+ read_unlock();
+ return size;
+ }
+
+ p = oe_in_pack(pack, e);
+ if (!p)
+ BUG("when e->type is a delta, it must belong to a pack");
+
+ read_lock();
+ w_curs = NULL;
+ buf = use_pack(p, &w_curs, e->in_pack_offset, &avail);
+ used = unpack_object_header_buffer(buf, avail, &type, &size);
+ if (used == 0)
+ die(_("unable to parse object header of %s"),
+ oid_to_hex(&e->idx.oid));
+
+ unuse_pack(&w_curs);
+ read_unlock();
+ return size;
+}
+
static int try_delta(struct unpacked *trg, struct unpacked *src,
unsigned max_depth, unsigned long *mem_usage)
{
void *delta_buf;
/* Don't bother doing diffs between different types */
- if (trg_entry->type != src_entry->type)
+ if (oe_type(trg_entry) != oe_type(src_entry))
return -1;
/*
* it, we will still save the transfer cost, as we already know
* the other side has it and we won't send src_entry at all.
*/
- if (reuse_delta && trg_entry->in_pack &&
- trg_entry->in_pack == src_entry->in_pack &&
+ if (reuse_delta && IN_PACK(trg_entry) &&
+ IN_PACK(trg_entry) == IN_PACK(src_entry) &&
!src_entry->preferred_base &&
trg_entry->in_pack_type != OBJ_REF_DELTA &&
trg_entry->in_pack_type != OBJ_OFS_DELTA)
return 0;
/* Now some size filtering heuristics. */
- trg_size = trg_entry->size;
- if (!trg_entry->delta) {
- max_size = trg_size/2 - 20;
+ trg_size = SIZE(trg_entry);
+ if (!DELTA(trg_entry)) {
+ max_size = trg_size/2 - the_hash_algo->rawsz;
ref_depth = 1;
} else {
- max_size = trg_entry->delta_size;
+ max_size = DELTA_SIZE(trg_entry);
ref_depth = trg->depth;
}
max_size = (uint64_t)max_size * (max_depth - src->depth) /
(max_depth - ref_depth + 1);
if (max_size == 0)
return 0;
- src_size = src_entry->size;
+ src_size = SIZE(src_entry);
sizediff = src_size < trg_size ? trg_size - src_size : 0;
if (sizediff >= max_size)
return 0;
delta_buf = create_delta(src->index, trg->data, trg_size, &delta_size, max_size);
if (!delta_buf)
return 0;
+ if (delta_size >= (1U << OE_DELTA_SIZE_BITS)) {
+ free(delta_buf);
+ return 0;
+ }
- if (trg_entry->delta) {
+ if (DELTA(trg_entry)) {
/* Prefer only shallower same-sized deltas. */
- if (delta_size == trg_entry->delta_size &&
+ if (delta_size == DELTA_SIZE(trg_entry) &&
src->depth + 1 >= trg->depth) {
free(delta_buf);
return 0;
free(trg_entry->delta_data);
cache_lock();
if (trg_entry->delta_data) {
- delta_cache_size -= trg_entry->delta_size;
+ delta_cache_size -= DELTA_SIZE(trg_entry);
trg_entry->delta_data = NULL;
}
if (delta_cacheable(src_size, trg_size, delta_size)) {
free(delta_buf);
}
- trg_entry->delta = src_entry;
- trg_entry->delta_size = delta_size;
+ SET_DELTA(trg_entry, src_entry);
+ SET_DELTA_SIZE(trg_entry, delta_size);
trg->depth = src->depth + 1;
return 1;
static unsigned int check_delta_limit(struct object_entry *me, unsigned int n)
{
- struct object_entry *child = me->delta_child;
+ struct object_entry *child = DELTA_CHILD(me);
unsigned int m = n;
while (child) {
unsigned int c = check_delta_limit(child, n + 1);
if (m < c)
m = c;
- child = child->delta_sibling;
+ child = DELTA_SIBLING(child);
}
return m;
}
free_delta_index(n->index);
n->index = NULL;
if (n->data) {
- freed_mem += n->entry->size;
+ freed_mem += SIZE(n->entry);
FREE_AND_NULL(n->data);
}
n->entry = NULL;
* otherwise they would become too deep.
*/
max_depth = depth;
- if (entry->delta_child) {
+ if (DELTA_CHILD(entry)) {
max_depth -= check_delta_limit(entry, 0);
if (max_depth <= 0)
goto next;
* between writes at that moment.
*/
if (entry->delta_data && !pack_to_stdout) {
- entry->z_delta_size = do_compress(&entry->delta_data,
- entry->delta_size);
- cache_lock();
- delta_cache_size -= entry->delta_size;
- delta_cache_size += entry->z_delta_size;
- cache_unlock();
+ unsigned long size;
+
+ size = do_compress(&entry->delta_data, DELTA_SIZE(entry));
+ if (size < (1U << OE_Z_DELTA_BITS)) {
+ entry->z_delta_size = size;
+ cache_lock();
+ delta_cache_size -= DELTA_SIZE(entry);
+ delta_cache_size += entry->z_delta_size;
+ cache_unlock();
+ } else {
+ FREE_AND_NULL(entry->delta_data);
+ entry->z_delta_size = 0;
+ }
}
/* if we made n a delta, and if n is already at max
* depth, leaving it in the window is pointless. we
* should evict it first.
*/
- if (entry->delta && max_depth <= n->depth)
+ if (DELTA(entry) && max_depth <= n->depth)
continue;
/*
* currently deltified object, to keep it longer. It will
* be the first base object to be attempted next.
*/
- if (entry->delta) {
+ if (DELTA(entry)) {
struct unpacked swap = array[best_base];
int dist = (window + idx - best_base) % window;
int dst = best_base;
for (i = 0; i < to_pack.nr_objects; i++) {
struct object_entry *entry = to_pack.objects + i;
- if (entry->delta)
+ if (DELTA(entry))
/* This happens if we decided to reuse existing
* delta from a pack. "reuse_delta &&" is implied.
*/
continue;
- if (entry->size < 50)
+ if (!entry->type_valid ||
+ oe_size_less_than(&to_pack, entry, 50))
continue;
if (entry->no_try_delta)
if (!entry->preferred_base) {
nr_deltas++;
- if (entry->type < 0)
+ if (oe_type(entry) < 0)
die("unable to get type of object %s",
oid_to_hex(&entry->idx.oid));
} else {
- if (entry->type < 0) {
+ if (oe_type(entry) < 0) {
/*
* This object is not found, but we
* don't have to include it anyway.
die("expected object ID, got garbage:\n %s", line);
add_preferred_base_object(p + 1);
- add_object_entry(&oid, 0, p + 1, 0);
+ add_object_entry(&oid, OBJ_NONE, p + 1, 0);
}
}
struct object_id oid;
struct object *o;
- if (!p->pack_local || p->pack_keep)
+ if (!p->pack_local || p->pack_keep || p->pack_keep_in_core)
continue;
if (open_pack_index(p))
die("cannot open pack index");
static int add_loose_object(const struct object_id *oid, const char *path,
void *data)
{
- enum object_type type = oid_object_info(oid, NULL);
+ enum object_type type = oid_object_info(the_repository, oid, NULL);
if (type < 0) {
warning("loose object at %s could not be examined", path);
get_packed_git(the_repository);
while (p) {
- if ((!p->pack_local || p->pack_keep) &&
+ if ((!p->pack_local || p->pack_keep ||
+ p->pack_keep_in_core) &&
find_pack_entry_one(oid->hash, p)) {
last_found = p;
return 1;
struct object_id oid;
for (p = get_packed_git(the_repository); p; p = p->next) {
- if (!p->pack_local || p->pack_keep)
+ if (!p->pack_local || p->pack_keep || p->pack_keep_in_core)
continue;
if (open_pack_index(p))
{
return pack_to_stdout &&
allow_ofs_delta &&
- !ignore_packed_keep &&
+ !ignore_packed_keep_on_disk &&
+ !ignore_packed_keep_in_core &&
(!local || !have_non_local_packs) &&
!incremental;
}
oid_array_clear(&recent_objects);
}
+static void add_extra_kept_packs(const struct string_list *names)
+{
+ struct packed_git *p;
+
+ if (!names->nr)
+ return;
+
+ for (p = get_packed_git(the_repository); p; p = p->next) {
+ const char *name = basename(p->pack_name);
+ int i;
+
+ if (!p->pack_local)
+ continue;
+
+ for (i = 0; i < names->nr; i++)
+ if (!fspathcmp(name, names->items[i].string))
+ break;
+
+ if (i < names->nr) {
+ p->pack_keep_in_core = 1;
+ ignore_packed_keep_in_core = 1;
+ continue;
+ }
+ }
+}
+
static int option_parse_index_version(const struct option *opt,
const char *arg, int unset)
{
struct argv_array rp = ARGV_ARRAY_INIT;
int rev_list_unpacked = 0, rev_list_all = 0, rev_list_reflog = 0;
int rev_list_index = 0;
+ struct string_list keep_pack_list = STRING_LIST_INIT_NODUP;
struct option pack_objects_options[] = {
OPT_SET_INT('q', "quiet", &progress,
N_("do not show progress meter"), 0),
N_("create thin packs")),
OPT_BOOL(0, "shallow", &shallow,
N_("create packs suitable for shallow fetches")),
- OPT_BOOL(0, "honor-pack-keep", &ignore_packed_keep,
+ OPT_BOOL(0, "honor-pack-keep", &ignore_packed_keep_on_disk,
N_("ignore packs that have companion .keep file")),
+ OPT_STRING_LIST(0, "keep-pack", &keep_pack_list, N_("name"),
+ N_("ignore this pack")),
OPT_INTEGER(0, "compression", &pack_compression_level,
N_("pack compression level")),
OPT_SET_INT(0, "keep-true-parents", &grafts_replace_parents,
OPT_END(),
};
+ if (DFS_NUM_STATES > (1 << OE_DFS_STATE_BITS))
+ BUG("too many dfs states, increase OE_DFS_STATE_BITS");
+
check_replace_refs = 0;
reset_pack_idx_option(&pack_idx_opts);
if (pack_to_stdout != !base_name || argc)
usage_with_options(pack_usage, pack_objects_options);
+ if (depth >= (1 << OE_DEPTH_BITS)) {
+ warning(_("delta chain depth %d is too deep, forcing %d"),
+ depth, (1 << OE_DEPTH_BITS) - 1);
+ depth = (1 << OE_DEPTH_BITS) - 1;
+ }
+ if (cache_max_small_delta_size >= (1U << OE_Z_DELTA_BITS)) {
+ warning(_("pack.deltaCacheLimit is too high, forcing %d"),
+ (1U << OE_Z_DELTA_BITS) - 1);
+ cache_max_small_delta_size = (1U << OE_Z_DELTA_BITS) - 1;
+ }
+
argv_array_push(&rp, "pack-objects");
if (thin) {
use_internal_rev_list = 1;
fetch_if_missing = 0;
argv_array_push(&rp, "--exclude-promisor-objects");
}
+ if (unpack_unreachable || keep_unreachable || pack_loose_unreachable)
+ use_internal_rev_list = 1;
if (!reuse_object)
reuse_delta = 0;
if (progress && all_progress_implied)
progress = 2;
- if (ignore_packed_keep) {
+ add_extra_kept_packs(&keep_pack_list);
+ if (ignore_packed_keep_on_disk) {
struct packed_git *p;
for (p = get_packed_git(the_repository); p; p = p->next)
if (p->pack_local && p->pack_keep)
break;
if (!p) /* no keep-able packs found */
- ignore_packed_keep = 0;
+ ignore_packed_keep_on_disk = 0;
}
if (local) {
/*
- * unlike ignore_packed_keep above, we do not want to
- * unset "local" based on looking at packs, as it
- * also covers non-local objects
+ * unlike ignore_packed_keep_on_disk above, we do not
+ * want to unset "local" based on looking at packs, as
+ * it also covers non-local objects
*/
struct packed_git *p;
for (p = get_packed_git(the_repository); p; p = p->next) {
}
}
+ prepare_packing_data(&to_pack);
+
if (progress)
- progress_state = start_progress(_("Counting objects"), 0);
+ progress_state = start_progress(_("Enumerating objects"), 0);
if (!use_internal_rev_list)
read_object_list_from_stdin();
else {
struct llist_item {
struct llist_item *next;
- const unsigned char *sha1;
+ const struct object_id *oid;
};
static struct llist {
struct llist_item *front;
return ret;
new_item = ret->front = llist_item_get();
- new_item->sha1 = list->front->sha1;
+ new_item->oid = list->front->oid;
old_item = list->front->next;
while (old_item) {
prev = new_item;
new_item = llist_item_get();
prev->next = new_item;
- new_item->sha1 = old_item->sha1;
+ new_item->oid = old_item->oid;
old_item = old_item->next;
}
new_item->next = NULL;
static inline struct llist_item *llist_insert(struct llist *list,
struct llist_item *after,
- const unsigned char *sha1)
+ const struct object_id *oid)
{
struct llist_item *new_item = llist_item_get();
- new_item->sha1 = sha1;
+ new_item->oid = oid;
new_item->next = NULL;
if (after != NULL) {
}
static inline struct llist_item *llist_insert_back(struct llist *list,
- const unsigned char *sha1)
+ const struct object_id *oid)
{
- return llist_insert(list, list->back, sha1);
+ return llist_insert(list, list->back, oid);
}
static inline struct llist_item *llist_insert_sorted_unique(struct llist *list,
- const unsigned char *sha1, struct llist_item *hint)
+ const struct object_id *oid, struct llist_item *hint)
{
struct llist_item *prev = NULL, *l;
l = (hint == NULL) ? list->front : hint;
while (l) {
- int cmp = hashcmp(l->sha1, sha1);
+ int cmp = oidcmp(l->oid, oid);
if (cmp > 0) { /* we insert before this entry */
- return llist_insert(list, prev, sha1);
+ return llist_insert(list, prev, oid);
}
if (!cmp) { /* already exists */
return l;
l = l->next;
}
/* insert at the end */
- return llist_insert_back(list, sha1);
+ return llist_insert_back(list, oid);
}
/* returns a pointer to an item in front of sha1 */
-static inline struct llist_item * llist_sorted_remove(struct llist *list, const unsigned char *sha1, struct llist_item *hint)
+static inline struct llist_item * llist_sorted_remove(struct llist *list, const struct object_id *oid, struct llist_item *hint)
{
struct llist_item *prev, *l;
l = (hint == NULL) ? list->front : hint;
prev = NULL;
while (l) {
- int cmp = hashcmp(l->sha1, sha1);
+ int cmp = oidcmp(l->oid, oid);
if (cmp > 0) /* not in list, since sorted */
return prev;
if (!cmp) { /* found */
b = B->front;
while (b) {
- hint = llist_sorted_remove(A, b->sha1, hint);
+ hint = llist_sorted_remove(A, b->oid, hint);
b = b->next;
}
}
unsigned long p1_off = 0, p2_off = 0, p1_step, p2_step;
const unsigned char *p1_base, *p2_base;
struct llist_item *p1_hint = NULL, *p2_hint = NULL;
+ const unsigned int hashsz = the_hash_algo->rawsz;
p1_base = p1->pack->index_data;
p2_base = p2->pack->index_data;
p1_base += 256 * 4 + ((p1->pack->index_version < 2) ? 4 : 8);
p2_base += 256 * 4 + ((p2->pack->index_version < 2) ? 4 : 8);
- p1_step = (p1->pack->index_version < 2) ? 24 : 20;
- p2_step = (p2->pack->index_version < 2) ? 24 : 20;
+ p1_step = hashsz + ((p1->pack->index_version < 2) ? 4 : 0);
+ p2_step = hashsz + ((p2->pack->index_version < 2) ? 4 : 0);
while (p1_off < p1->pack->num_objects * p1_step &&
p2_off < p2->pack->num_objects * p2_step)
/* cmp ~ p1 - p2 */
if (cmp == 0) {
p1_hint = llist_sorted_remove(p1->unique_objects,
- p1_base + p1_off, p1_hint);
+ (const struct object_id *)(p1_base + p1_off),
+ p1_hint);
p2_hint = llist_sorted_remove(p2->unique_objects,
- p1_base + p1_off, p2_hint);
+ (const struct object_id *)(p1_base + p1_off),
+ p2_hint);
p1_off += p1_step;
p2_off += p2_step;
continue;
size_t ret = 0;
unsigned long p1_off = 0, p2_off = 0, p1_step, p2_step;
const unsigned char *p1_base, *p2_base;
+ const unsigned int hashsz = the_hash_algo->rawsz;
p1_base = p1->index_data;
p2_base = p2->index_data;
p1_base += 256 * 4 + ((p1->index_version < 2) ? 4 : 8);
p2_base += 256 * 4 + ((p2->index_version < 2) ? 4 : 8);
- p1_step = (p1->index_version < 2) ? 24 : 20;
- p2_step = (p2->index_version < 2) ? 24 : 20;
+ p1_step = hashsz + ((p1->index_version < 2) ? 4 : 0);
+ p2_step = hashsz + ((p2->index_version < 2) ? 4 : 0);
while (p1_off < p1->num_objects * p1_step &&
p2_off < p2->num_objects * p2_step)
l = pl->all_objects->front;
while (l) {
hint = llist_insert_sorted_unique(all_objects,
- l->sha1, hint);
+ l->oid, hint);
l = l->next;
}
pl = pl->next;
base = p->index_data;
base += 256 * 4 + ((p->index_version < 2) ? 4 : 8);
- step = (p->index_version < 2) ? 24 : 20;
+ step = the_hash_algo->rawsz + ((p->index_version < 2) ? 4 : 0);
while (off < p->num_objects * step) {
- llist_insert_back(l.all_objects, base + off);
+ llist_insert_back(l.all_objects, (const struct object_id *)(base + off));
off += step;
}
/* this list will be pruned in cmp_two_packs later */
int i;
struct pack_list *min, *red, *pl;
struct llist *ignore;
- unsigned char *sha1;
- char buf[42]; /* 40 byte sha1 + \n + \0 */
+ struct object_id *oid;
+ char buf[GIT_MAX_HEXSZ + 2]; /* hex hash + \n + \0 */
if (argc == 2 && !strcmp(argv[1], "-h"))
usage(pack_redundant_usage);
llist_init(&ignore);
if (!isatty(0)) {
while (fgets(buf, sizeof(buf), stdin)) {
- sha1 = xmalloc(20);
- if (get_sha1_hex(buf, sha1))
- die("Bad sha1 on stdin: %s", buf);
- llist_insert_sorted_unique(ignore, sha1, NULL);
+ oid = xmalloc(sizeof(*oid));
+ if (get_oid_hex(buf, oid))
+ die("Bad object ID on stdin: %s", buf);
+ llist_insert_sorted_unique(ignore, oid, NULL);
}
}
llist_sorted_difference_inplace(all_objects, ignore);
{
int *opts = data;
- if (!has_sha1_pack(oid->hash))
+ if (!has_object_pack(oid))
return 0;
if (*opts & PRUNE_PACKED_DRY_RUN)
if (st.st_mtime > expire)
return 0;
if (show_only || verbose) {
- enum object_type type = oid_object_info(oid, NULL);
+ enum object_type type = oid_object_info(the_repository, oid,
+ NULL);
printf("%s %s\n", oid_to_hex(oid),
(type > 0) ? type_name(type) : "unknown");
}
#include "remote.h"
#include "dir.h"
#include "refs.h"
+#include "refspec.h"
#include "revision.h"
#include "submodule.h"
#include "submodule-config.h"
REBASE_FALSE = 0,
REBASE_TRUE,
REBASE_PRESERVE,
+ REBASE_MERGES,
REBASE_INTERACTIVE
};
/**
* Parses the value of --rebase. If value is a false value, returns
* REBASE_FALSE. If value is a true value, returns REBASE_TRUE. If value is
- * "preserve", returns REBASE_PRESERVE. If value is a invalid value, dies with
- * a fatal error if fatal is true, otherwise returns REBASE_INVALID.
+ * "merges", returns REBASE_MERGES. If value is "preserve", returns
+ * REBASE_PRESERVE. If value is a invalid value, dies with a fatal error if
+ * fatal is true, otherwise returns REBASE_INVALID.
*/
static enum rebase_type parse_config_rebase(const char *key, const char *value,
int fatal)
return REBASE_TRUE;
else if (!strcmp(value, "preserve"))
return REBASE_PRESERVE;
+ else if (!strcmp(value, "merges"))
+ return REBASE_MERGES;
else if (!strcmp(value, "interactive"))
return REBASE_INTERACTIVE;
/* Options passed to git-merge or git-rebase */
OPT_GROUP(N_("Options related to merging")),
{ OPTION_CALLBACK, 'r', "rebase", &opt_rebase,
- "false|true|preserve|interactive",
+ "false|true|merges|preserve|interactive",
N_("incorporate changes by rebasing rather than merging"),
PARSE_OPT_OPTARG, parse_opt_rebase },
OPT_PASSTHRU('n', NULL, &opt_diffstat, NULL,
argv_array_push(&args, repo);
argv_array_pushv(&args, refspecs);
} else if (*refspecs)
- die("BUG: refspecs without repo?");
+ BUG("refspecs without repo?");
ret = run_command_v_opt(args.argv, RUN_GIT_CMD);
argv_array_clear(&args);
return ret;
*/
static const char *get_tracking_branch(const char *remote, const char *refspec)
{
- struct refspec *spec;
+ struct refspec_item spec;
const char *spec_src;
const char *merge_branch;
- spec = parse_fetch_refspec(1, &refspec);
- spec_src = spec->src;
+ refspec_item_init(&spec, refspec, REFSPEC_FETCH);
+ spec_src = spec.src;
if (!*spec_src || !strcmp(spec_src, "HEAD"))
spec_src = "HEAD";
else if (skip_prefix(spec_src, "heads/", &spec_src))
} else
merge_branch = NULL;
- free_refspec(1, spec);
+ refspec_item_clear(&spec);
return merge_branch;
}
argv_push_verbosity(&args);
/* Options passed to git-rebase */
- if (opt_rebase == REBASE_PRESERVE)
+ if (opt_rebase == REBASE_MERGES)
+ argv_array_push(&args, "--rebase-merges");
+ else if (opt_rebase == REBASE_PRESERVE)
argv_array_push(&args, "--preserve-merges");
else if (opt_rebase == REBASE_INTERACTIVE)
argv_array_push(&args, "--interactive");
#include "cache.h"
#include "config.h"
#include "refs.h"
+#include "refspec.h"
#include "run-command.h"
#include "builtin.h"
#include "remote.h"
static struct push_cas_option cas;
-static const char **refspec;
-static int refspec_nr;
-static int refspec_alloc;
+static struct refspec rs = REFSPEC_INIT_PUSH;
static struct string_list push_options_config = STRING_LIST_INIT_DUP;
-static void add_refspec(const char *ref)
-{
- refspec_nr++;
- ALLOC_GROW(refspec, refspec_nr, refspec_alloc);
- refspec[refspec_nr-1] = ref;
-}
-
static const char *map_refspec(const char *ref,
struct remote *remote, struct ref *local_refs)
{
if (count_refspec_match(ref, local_refs, &matched) != 1)
return ref;
- if (remote->push) {
- struct refspec query;
- memset(&query, 0, sizeof(struct refspec));
+ if (remote->push.nr) {
+ struct refspec_item query;
+ memset(&query, 0, sizeof(struct refspec_item));
query.src = matched->name;
- if (!query_refspecs(remote->push, remote->push_refspec_nr, &query) &&
- query.dst) {
+ if (!query_refspecs(&remote->push, &query) && query.dst) {
struct strbuf buf = STRBUF_INIT;
strbuf_addf(&buf, "%s%s:%s",
query.force ? "+" : "",
}
ref = map_refspec(ref, remote, local_refs);
}
- add_refspec(ref);
+ refspec_append(&rs, ref);
}
}
}
strbuf_addf(&refspec, "%s:%s", branch->refname, branch->merge[0]->src);
- add_refspec(refspec.buf);
+ refspec_append(&rs, refspec.buf);
}
static void setup_push_current(struct remote *remote, struct branch *branch)
if (!branch)
die(_(message_detached_head_die), remote->name);
strbuf_addf(&refspec, "%s:%s", branch->refname, branch->refname);
- add_refspec(refspec.buf);
+ refspec_append(&rs, refspec.buf);
}
static int is_workflow_triangular(struct remote *remote)
switch (push_default) {
default:
case PUSH_DEFAULT_MATCHING:
- add_refspec(":");
+ refspec_append(&rs, ":");
break;
case PUSH_DEFAULT_UNSPECIFIED:
advise(_(message_advice_ref_needs_force));
}
-static int push_with_options(struct transport *transport, int flags)
+static int push_with_options(struct transport *transport, struct refspec *rs,
+ int flags)
{
int err;
unsigned int reject_reasons;
if (verbosity > 0)
fprintf(stderr, _("Pushing to %s\n"), transport->url);
- err = transport_push(transport, refspec_nr, refspec, flags,
- &reject_reasons);
+ err = transport_push(transport, rs, flags, &reject_reasons);
if (err != 0) {
fprintf(stderr, "%s", push_get_color(PUSH_COLOR_ERROR));
error(_("failed to push some refs to '%s'"), transport->url);
struct remote *remote = pushremote_get(repo);
const char **url;
int url_nr;
+ struct refspec *push_refspec = &rs;
if (!remote) {
if (repo)
if (push_options->nr)
flags |= TRANSPORT_PUSH_OPTIONS;
- if ((flags & TRANSPORT_PUSH_ALL) && refspec) {
- if (!strcmp(*refspec, "refs/tags/*"))
- return error(_("--all and --tags are incompatible"));
- return error(_("--all can't be combined with refspecs"));
- }
-
- if ((flags & TRANSPORT_PUSH_MIRROR) && refspec) {
- if (!strcmp(*refspec, "refs/tags/*"))
- return error(_("--mirror and --tags are incompatible"));
- return error(_("--mirror can't be combined with refspecs"));
- }
-
- if ((flags & (TRANSPORT_PUSH_ALL|TRANSPORT_PUSH_MIRROR)) ==
- (TRANSPORT_PUSH_ALL|TRANSPORT_PUSH_MIRROR)) {
- return error(_("--all and --mirror are incompatible"));
- }
-
- if (!refspec && !(flags & TRANSPORT_PUSH_ALL)) {
- if (remote->push_refspec_nr) {
- refspec = remote->push_refspec;
- refspec_nr = remote->push_refspec_nr;
+ if (!push_refspec->nr && !(flags & TRANSPORT_PUSH_ALL)) {
+ if (remote->push.nr) {
+ push_refspec = &remote->push;
} else if (!(flags & TRANSPORT_PUSH_MIRROR))
setup_default_push_refspecs(remote);
}
transport_get(remote, url[i]);
if (flags & TRANSPORT_PUSH_OPTIONS)
transport->push_options = push_options;
- if (push_with_options(transport, flags))
+ if (push_with_options(transport, push_refspec, flags))
errs++;
}
} else {
transport_get(remote, NULL);
if (flags & TRANSPORT_PUSH_OPTIONS)
transport->push_options = push_options;
- if (push_with_options(transport, flags))
+ if (push_with_options(transport, push_refspec, flags))
errs++;
}
return !!errs;
die(_("--delete is incompatible with --all, --mirror and --tags"));
if (deleterefs && argc < 2)
die(_("--delete doesn't make sense without any refs"));
+ if (flags & TRANSPORT_PUSH_ALL) {
+ if (tags)
+ die(_("--all and --tags are incompatible"));
+ if (argc >= 2)
+ die(_("--all can't be combined with refspecs"));
+ }
+ if (flags & TRANSPORT_PUSH_MIRROR) {
+ if (tags)
+ die(_("--mirror and --tags are incompatible"));
+ if (argc >= 2)
+ die(_("--mirror can't be combined with refspecs"));
+ }
+ if ((flags & TRANSPORT_PUSH_ALL) && (flags & TRANSPORT_PUSH_MIRROR))
+ die(_("--all and --mirror are incompatible"));
if (recurse_submodules == RECURSE_SUBMODULES_CHECK)
flags |= TRANSPORT_RECURSE_SUBMODULES_CHECK;
flags |= TRANSPORT_RECURSE_SUBMODULES_ONLY;
if (tags)
- add_refspec("refs/tags/*");
+ refspec_append(&rs, "refs/tags/*");
if (argc > 0) {
repo = argv[0];
return git_default_config(var, value, cb);
}
-static struct lock_file lock_file;
-
int cmd_read_tree(int argc, const char **argv, const char *unused_prefix)
{
int i, stage = 0;
struct tree_desc t[MAX_UNPACK_TREES];
struct unpack_trees_options opts;
int prefix_set = 0;
+ struct lock_file lock_file = LOCK_INIT;
const struct option read_tree_options[] = {
{ OPTION_CALLBACK, 0, "index-output", NULL, N_("file"),
N_("write resulting index to <file>"),
int cmd_rebase__helper(int argc, const char **argv, const char *prefix)
{
struct replay_opts opts = REPLAY_OPTS_INIT;
- unsigned flags = 0, keep_empty = 0;
- int abbreviate_commands = 0;
+ unsigned flags = 0, keep_empty = 0, rebase_merges = 0;
+ int abbreviate_commands = 0, rebase_cousins = -1;
enum {
CONTINUE = 1, ABORT, MAKE_SCRIPT, SHORTEN_OIDS, EXPAND_OIDS,
CHECK_TODO_LIST, SKIP_UNNECESSARY_PICKS, REARRANGE_SQUASH,
OPT_BOOL(0, "keep-empty", &keep_empty, N_("keep empty commits")),
OPT_BOOL(0, "allow-empty-message", &opts.allow_empty_message,
N_("allow commits with empty messages")),
+ OPT_BOOL(0, "rebase-merges", &rebase_merges, N_("rebase merge commits")),
+ OPT_BOOL(0, "rebase-cousins", &rebase_cousins,
+ N_("keep original branch points of cousins")),
OPT_CMDMODE(0, "continue", &command, N_("continue rebase"),
CONTINUE),
OPT_CMDMODE(0, "abort", &command, N_("abort rebase"),
flags |= keep_empty ? TODO_LIST_KEEP_EMPTY : 0;
flags |= abbreviate_commands ? TODO_LIST_ABBREVIATE_CMDS : 0;
+ flags |= rebase_merges ? TODO_LIST_REBASE_MERGES : 0;
+ flags |= rebase_cousins > 0 ? TODO_LIST_REBASE_COUSINS : 0;
flags |= command == SHORTEN_OIDS ? TODO_LIST_SHORTEN_IDS : 0;
+ if (rebase_cousins >= 0 && !rebase_merges)
+ warning(_("--[no-]rebase-cousins has no effect without "
+ "--rebase-merges"));
+
if (command == CONTINUE && argc == 1)
return !!sequencer_continue(&opts);
if (command == ABORT && argc == 1)
/* RFC 2104 2. (6) & (7) */
git_SHA1_Init(&ctx);
git_SHA1_Update(&ctx, k_opad, sizeof(k_opad));
- git_SHA1_Update(&ctx, out, 20);
+ git_SHA1_Update(&ctx, out, GIT_SHA1_RAWSZ);
git_SHA1_Final(out, &ctx);
}
static char *prepare_push_cert_nonce(const char *path, timestamp_t stamp)
{
struct strbuf buf = STRBUF_INIT;
- unsigned char sha1[20];
+ unsigned char sha1[GIT_SHA1_RAWSZ];
strbuf_addf(&buf, "%s:%"PRItime, path, stamp);
hmac_sha1(sha1, buf.buf, buf.len, cert_nonce_seed, strlen(cert_nonce_seed));;
strbuf_release(&buf);
/* RFC 2104 5. HMAC-SHA1-80 */
- strbuf_addf(&buf, "%"PRItime"-%.*s", stamp, 20, sha1_to_hex(sha1));
+ strbuf_addf(&buf, "%"PRItime"-%.*s", stamp, GIT_SHA1_HEXSZ, sha1_to_hex(sha1));
return strbuf_detach(&buf, NULL);
}
static int command_singleton_iterator(void *cb_data, struct object_id *oid);
static int update_shallow_ref(struct command *cmd, struct shallow_info *si)
{
- static struct lock_file shallow_lock;
+ struct lock_file shallow_lock = LOCK_INIT;
struct oid_array extra = OID_ARRAY_INIT;
struct check_connected_options opt = CHECK_CONNECTED_INIT;
uint32_t mask = 1 << (cmd->index % 32);
return "Working directory has unstaged changes";
/* diff-index with either HEAD or an empty tree */
- diff_index[4] = head_has_history() ? "HEAD" : EMPTY_TREE_SHA1_HEX;
+ diff_index[4] = head_has_history() ? "HEAD" : empty_tree_oid_hex();
child_process_init(&child);
child.argv = diff_index;
}
}
if (!checked_connectivity)
- die("BUG: connectivity check skipped???");
+ BUG("connectivity check skipped???");
}
static void execute_commands_non_atomic(struct command *commands,
for (i = 0; i < found.nr; i++) {
struct commit *c =
(struct commit *)found.objects[i].item;
- if (!tree_is_complete(&c->tree->object.oid)) {
+ if (!tree_is_complete(get_commit_tree_oid(c))) {
is_incomplete = 1;
c->object.flags |= INCOMPLETE;
}
#include "strbuf.h"
#include "run-command.h"
#include "refs.h"
+#include "refspec.h"
#include "argv-array.h"
static const char * const builtin_remote_usage[] = {
struct branch_info {
char *remote_name;
struct string_list merge;
- enum { NO_REBASE, NORMAL_REBASE, INTERACTIVE_REBASE } rebase;
+ enum {
+ NO_REBASE, NORMAL_REBASE, INTERACTIVE_REBASE, REBASE_MERGES
+ } rebase;
};
static struct string_list branch_list = STRING_LIST_INIT_NODUP;
info->rebase = v;
else if (!strcmp(value, "preserve"))
info->rebase = NORMAL_REBASE;
+ else if (!strcmp(value, "merges"))
+ info->rebase = REBASE_MERGES;
else if (!strcmp(value, "interactive"))
info->rebase = INTERACTIVE_REBASE;
}
struct ref *ref, *stale_refs;
int i;
- for (i = 0; i < states->remote->fetch_refspec_nr; i++)
- if (get_fetch_map(remote_refs, states->remote->fetch + i, &tail, 1))
+ for (i = 0; i < states->remote->fetch.nr; i++)
+ if (get_fetch_map(remote_refs, &states->remote->fetch.items[i], &tail, 1))
die(_("Could not get fetch map for refspec %s"),
- states->remote->fetch_refspec[i]);
+ states->remote->fetch.raw[i]);
states->new_refs.strdup_strings = 1;
states->tracked.strdup_strings = 1;
else
string_list_append(&states->tracked, abbrev_branch(ref->name));
}
- stale_refs = get_stale_heads(states->remote->fetch,
- states->remote->fetch_refspec_nr, fetch_map);
+ stale_refs = get_stale_heads(&states->remote->fetch, fetch_map);
for (ref = stale_refs; ref; ref = ref->next) {
struct string_list_item *item =
string_list_append(&states->stale, abbrev_branch(ref->name));
local_refs = get_local_heads();
push_map = copy_ref_list(remote_refs);
- match_push_refs(local_refs, &push_map, remote->push_refspec_nr,
- remote->push_refspec, MATCH_REFS_NONE);
+ match_push_refs(local_refs, &push_map, &remote->push, MATCH_REFS_NONE);
states->push.strdup_strings = 1;
for (ref = push_map; ref; ref = ref->next) {
return 0;
states->push.strdup_strings = 1;
- if (!remote->push_refspec_nr) {
+ if (!remote->push.nr) {
item = string_list_append(&states->push, _("(matching)"));
info = item->util = xcalloc(1, sizeof(struct push_info));
info->status = PUSH_STATUS_NOTQUERIED;
info->dest = xstrdup(item->string);
}
- for (i = 0; i < remote->push_refspec_nr; i++) {
- struct refspec *spec = remote->push + i;
+ for (i = 0; i < remote->push.nr; i++) {
+ const struct refspec_item *spec = &remote->push.items[i];
if (spec->matching)
item = string_list_append(&states->push, _("(matching)"));
else if (strlen(spec->src))
{
struct ref *ref, *matches;
struct ref *fetch_map = NULL, **fetch_map_tail = &fetch_map;
- struct refspec refspec;
+ struct refspec_item refspec;
refspec.force = 0;
refspec.pattern = 1;
const struct object_id *oid, int flags, void *cb_data)
{
struct branches_for_remote *branches = cb_data;
- struct refspec refspec;
+ struct refspec_item refspec;
struct known_remote *kr;
memset(&refspec, 0, sizeof(refspec));
git_config_set_multivar(buf.buf, remote->url[i], "^$", 0);
strbuf_reset(&buf);
strbuf_addf(&buf, "remote.%s.push", remote->name);
- for (i = 0; i < remote->push_refspec_nr; i++)
- git_config_set_multivar(buf.buf, remote->push_refspec[i], "^$", 0);
+ for (i = 0; i < remote->push.raw_nr; i++)
+ git_config_set_multivar(buf.buf, remote->push.raw[i], "^$", 0);
strbuf_reset(&buf);
strbuf_addf(&buf, "remote.%s.fetch", remote->name);
- for (i = 0; i < remote->fetch_refspec_nr; i++)
- git_config_set_multivar(buf.buf, remote->fetch_refspec[i], "^$", 0);
+ for (i = 0; i < remote->fetch.raw_nr; i++)
+ git_config_set_multivar(buf.buf, remote->fetch.raw[i], "^$", 0);
if (remote->origin == REMOTE_REMOTES)
unlink_or_warn(git_path("remotes/%s", remote->name));
else if (remote->origin == REMOTE_BRANCHES)
strbuf_addf(&buf, "remote.%s.fetch", rename.new_name);
git_config_set_multivar(buf.buf, NULL, NULL, 1);
strbuf_addf(&old_remote_context, ":refs/remotes/%s/", rename.old_name);
- for (i = 0; i < oldremote->fetch_refspec_nr; i++) {
+ for (i = 0; i < oldremote->fetch.raw_nr; i++) {
char *ptr;
strbuf_reset(&buf2);
- strbuf_addstr(&buf2, oldremote->fetch_refspec[i]);
+ strbuf_addstr(&buf2, oldremote->fetch.raw[i]);
ptr = strstr(buf2.buf, old_remote_context.buf);
if (ptr) {
refspec_updated = 1;
const struct object_id *oid, int flags, void *cb_data)
{
struct ref_states *states = cb_data;
- struct refspec refspec;
+ struct refspec_item refspec;
if (flags & REF_ISSYMREF)
return 0;
printf(" %-*s ", show_info->width, item->string);
if (branch_info->rebase) {
- printf_ln(branch_info->rebase == INTERACTIVE_REBASE
- ? _("rebases interactively onto remote %s")
- : _("rebases onto remote %s"), merge->items[0].string);
+ const char *msg;
+ if (branch_info->rebase == INTERACTIVE_REBASE)
+ msg = _("rebases interactively onto remote %s");
+ else if (branch_info->rebase == REBASE_MERGES)
+ msg = _("rebases interactively (with merges) onto "
+ "remote %s");
+ else
+ msg = _("rebases onto remote %s");
+ printf_ln(msg, merge->items[0].string);
return 0;
} else if (show_info->any_rebase) {
printf_ln(_(" merges with remote %s"), merge->items[0].string);
* have a corresponding .keep or .promisor file. These packs are not to
* be kept if we are going to pack everything into one file.
*/
-static void get_non_kept_pack_filenames(struct string_list *fname_list)
+static void get_non_kept_pack_filenames(struct string_list *fname_list,
+ const struct string_list *extra_keep)
{
DIR *dir;
struct dirent *e;
while ((e = readdir(dir)) != NULL) {
size_t len;
+ int i;
+
+ for (i = 0; i < extra_keep->nr; i++)
+ if (!fspathcmp(e->d_name, extra_keep->items[i].string))
+ break;
+ if (extra_keep->nr > 0 && i < extra_keep->nr)
+ continue;
+
if (!strip_suffix(e->d_name, ".pack", &len))
continue;
struct string_list rollback = STRING_LIST_INIT_NODUP;
struct string_list existing_packs = STRING_LIST_INIT_DUP;
struct strbuf line = STRBUF_INIT;
- int ext, ret, failed;
+ int i, ext, ret, failed;
FILE *out;
/* variables to be filled by option parsing */
const char *depth = NULL;
const char *threads = NULL;
const char *max_pack_size = NULL;
+ struct string_list keep_pack_list = STRING_LIST_INIT_NODUP;
int no_reuse_delta = 0, no_reuse_object = 0;
int no_update_server_info = 0;
int quiet = 0;
N_("maximum size of each packfile")),
OPT_BOOL(0, "pack-kept-objects", &pack_kept_objects,
N_("repack objects in packs marked with .keep")),
+ OPT_STRING_LIST(0, "keep-pack", &keep_pack_list, N_("name"),
+ N_("do not repack this pack")),
OPT_END()
};
argv_array_push(&cmd.args, "--keep-true-parents");
if (!pack_kept_objects)
argv_array_push(&cmd.args, "--honor-pack-keep");
+ for (i = 0; i < keep_pack_list.nr; i++)
+ argv_array_pushf(&cmd.args, "--keep-pack=%s",
+ keep_pack_list.items[i].string);
argv_array_push(&cmd.args, "--non-empty");
argv_array_push(&cmd.args, "--all");
argv_array_push(&cmd.args, "--reflog");
argv_array_push(&cmd.args, "--write-bitmap-index");
if (pack_everything & ALL_INTO_ONE) {
- get_non_kept_pack_filenames(&existing_packs);
+ get_non_kept_pack_filenames(&existing_packs, &keep_pack_list);
if (existing_packs.nr && delete_redundant) {
if (unpack_unreachable) {
N_("git replace [-f] <object> <replacement>"),
N_("git replace [-f] --edit <object>"),
N_("git replace [-f] --graft <commit> [<parent>...]"),
+ N_("git replace [-f] --convert-graft-file"),
N_("git replace -d <object>..."),
N_("git replace [--format=<format>] [-l [<pattern>]]"),
NULL
if (get_oid(refname, &object))
return error("Failed to resolve '%s' as a valid ref.", refname);
- obj_type = oid_object_info(&object, NULL);
- repl_type = oid_object_info(oid, NULL);
+ obj_type = oid_object_info(the_repository, &object,
+ NULL);
+ repl_type = oid_object_info(the_repository, oid, NULL);
printf("%s (%s) -> %s (%s)\n", refname, type_name(obj_type),
oid_to_hex(oid), type_name(repl_type));
else if (!strcmp(format, "long"))
data.format = REPLACE_FORMAT_LONG;
else
- die("invalid replace format '%s'\n"
- "valid formats are 'short', 'medium' and 'long'\n",
- format);
+ return error("invalid replace format '%s'\n"
+ "valid formats are 'short', 'medium' and 'long'\n",
+ format);
for_each_replace_ref(the_repository, show_reference, (void *)&data);
return 0;
}
-static void check_ref_valid(struct object_id *object,
+static int check_ref_valid(struct object_id *object,
struct object_id *prev,
struct strbuf *ref,
int force)
strbuf_reset(ref);
strbuf_addf(ref, "%s%s", git_replace_ref_base, oid_to_hex(object));
if (check_refname_format(ref->buf, 0))
- die("'%s' is not a valid ref name.", ref->buf);
+ return error("'%s' is not a valid ref name.", ref->buf);
if (read_ref(ref->buf, prev))
oidclr(prev);
else if (!force)
- die("replace ref '%s' already exists", ref->buf);
+ return error("replace ref '%s' already exists", ref->buf);
+ return 0;
}
static int replace_object_oid(const char *object_ref,
struct strbuf ref = STRBUF_INIT;
struct ref_transaction *transaction;
struct strbuf err = STRBUF_INIT;
+ int res = 0;
- obj_type = oid_object_info(object, NULL);
- repl_type = oid_object_info(repl, NULL);
+ obj_type = oid_object_info(the_repository, object, NULL);
+ repl_type = oid_object_info(the_repository, repl, NULL);
if (!force && obj_type != repl_type)
- die("Objects must be of the same type.\n"
- "'%s' points to a replaced object of type '%s'\n"
- "while '%s' points to a replacement object of type '%s'.",
- object_ref, type_name(obj_type),
- replace_ref, type_name(repl_type));
-
- check_ref_valid(object, &prev, &ref, force);
+ return error("Objects must be of the same type.\n"
+ "'%s' points to a replaced object of type '%s'\n"
+ "while '%s' points to a replacement object of "
+ "type '%s'.",
+ object_ref, type_name(obj_type),
+ replace_ref, type_name(repl_type));
+
+ if (check_ref_valid(object, &prev, &ref, force)) {
+ strbuf_release(&ref);
+ return -1;
+ }
transaction = ref_transaction_begin(&err);
if (!transaction ||
ref_transaction_update(transaction, ref.buf, repl, &prev,
0, NULL, &err) ||
ref_transaction_commit(transaction, &err))
- die("%s", err.buf);
+ res = error("%s", err.buf);
ref_transaction_free(transaction);
strbuf_release(&ref);
- return 0;
+ return res;
}
static int replace_object(const char *object_ref, const char *replace_ref, int force)
struct object_id object, repl;
if (get_oid(object_ref, &object))
- die("Failed to resolve '%s' as a valid ref.", object_ref);
+ return error("Failed to resolve '%s' as a valid ref.",
+ object_ref);
if (get_oid(replace_ref, &repl))
- die("Failed to resolve '%s' as a valid ref.", replace_ref);
+ return error("Failed to resolve '%s' as a valid ref.",
+ replace_ref);
return replace_object_oid(object_ref, &object, replace_ref, &repl, force);
}
* If "raw" is true, then the object's raw contents are printed according to
* "type". Otherwise, we pretty-print the contents for human editing.
*/
-static void export_object(const struct object_id *oid, enum object_type type,
+static int export_object(const struct object_id *oid, enum object_type type,
int raw, const char *filename)
{
struct child_process cmd = CHILD_PROCESS_INIT;
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0)
- die_errno("unable to open %s for writing", filename);
+ return error_errno("unable to open %s for writing", filename);
argv_array_push(&cmd.args, "--no-replace-objects");
argv_array_push(&cmd.args, "cat-file");
cmd.out = fd;
if (run_command(&cmd))
- die("cat-file reported failure");
+ return error("cat-file reported failure");
+ return 0;
}
/*
* interpreting it as "type", and writing the result to the object database.
* The sha1 of the written object is returned via sha1.
*/
-static void import_object(struct object_id *oid, enum object_type type,
+static int import_object(struct object_id *oid, enum object_type type,
int raw, const char *filename)
{
int fd;
fd = open(filename, O_RDONLY);
if (fd < 0)
- die_errno("unable to open %s for reading", filename);
+ return error_errno("unable to open %s for reading", filename);
if (!raw && type == OBJ_TREE) {
const char *argv[] = { "mktree", NULL };
cmd.in = fd;
cmd.out = -1;
- if (start_command(&cmd))
- die("unable to spawn mktree");
+ if (start_command(&cmd)) {
+ close(fd);
+ return error("unable to spawn mktree");
+ }
- if (strbuf_read(&result, cmd.out, 41) < 0)
- die_errno("unable to read from mktree");
+ if (strbuf_read(&result, cmd.out, 41) < 0) {
+ error_errno("unable to read from mktree");
+ close(fd);
+ close(cmd.out);
+ return -1;
+ }
close(cmd.out);
- if (finish_command(&cmd))
- die("mktree reported failure");
- if (get_oid_hex(result.buf, oid) < 0)
- die("mktree did not return an object name");
+ if (finish_command(&cmd)) {
+ strbuf_release(&result);
+ return error("mktree reported failure");
+ }
+ if (get_oid_hex(result.buf, oid) < 0) {
+ strbuf_release(&result);
+ return error("mktree did not return an object name");
+ }
strbuf_release(&result);
} else {
struct stat st;
int flags = HASH_FORMAT_CHECK | HASH_WRITE_OBJECT;
- if (fstat(fd, &st) < 0)
- die_errno("unable to fstat %s", filename);
+ if (fstat(fd, &st) < 0) {
+ error_errno("unable to fstat %s", filename);
+ close(fd);
+ return -1;
+ }
if (index_fd(oid, fd, &st, type, NULL, flags) < 0)
- die("unable to write object to database");
+ return error("unable to write object to database");
/* index_fd close()s fd for us */
}
* No need to close(fd) here; both run-command and index-fd
* will have done it for us.
*/
+ return 0;
}
static int edit_and_replace(const char *object_ref, int force, int raw)
{
- char *tmpfile = git_pathdup("REPLACE_EDITOBJ");
+ char *tmpfile;
enum object_type type;
struct object_id old_oid, new_oid, prev;
struct strbuf ref = STRBUF_INIT;
if (get_oid(object_ref, &old_oid) < 0)
- die("Not a valid object name: '%s'", object_ref);
+ return error("Not a valid object name: '%s'", object_ref);
- type = oid_object_info(&old_oid, NULL);
+ type = oid_object_info(the_repository, &old_oid, NULL);
if (type < 0)
- die("unable to get object type for %s", oid_to_hex(&old_oid));
+ return error("unable to get object type for %s",
+ oid_to_hex(&old_oid));
- check_ref_valid(&old_oid, &prev, &ref, force);
+ if (check_ref_valid(&old_oid, &prev, &ref, force)) {
+ strbuf_release(&ref);
+ return -1;
+ }
strbuf_release(&ref);
- export_object(&old_oid, type, raw, tmpfile);
- if (launch_editor(tmpfile, NULL, NULL) < 0)
- die("editing object file failed");
- import_object(&new_oid, type, raw, tmpfile);
-
+ tmpfile = git_pathdup("REPLACE_EDITOBJ");
+ if (export_object(&old_oid, type, raw, tmpfile)) {
+ free(tmpfile);
+ return -1;
+ }
+ if (launch_editor(tmpfile, NULL, NULL) < 0) {
+ free(tmpfile);
+ return error("editing object file failed");
+ }
+ if (import_object(&new_oid, type, raw, tmpfile)) {
+ free(tmpfile);
+ return -1;
+ }
free(tmpfile);
if (!oidcmp(&old_oid, &new_oid))
return replace_object_oid(object_ref, &old_oid, "replacement", &new_oid, force);
}
-static void replace_parents(struct strbuf *buf, int argc, const char **argv)
+static int replace_parents(struct strbuf *buf, int argc, const char **argv)
{
struct strbuf new_parents = STRBUF_INIT;
const char *parent_start, *parent_end;
/* prepare new parents */
for (i = 0; i < argc; i++) {
struct object_id oid;
- if (get_oid(argv[i], &oid) < 0)
- die(_("Not a valid object name: '%s'"), argv[i]);
- lookup_commit_or_die(&oid, argv[i]);
+ if (get_oid(argv[i], &oid) < 0) {
+ strbuf_release(&new_parents);
+ return error(_("Not a valid object name: '%s'"),
+ argv[i]);
+ }
+ if (!lookup_commit_reference(&oid)) {
+ strbuf_release(&new_parents);
+ return error(_("could not parse %s"), argv[i]);
+ }
strbuf_addf(&new_parents, "parent %s\n", oid_to_hex(&oid));
}
new_parents.buf, new_parents.len);
strbuf_release(&new_parents);
+ return 0;
}
struct check_mergetag_data {
const char **argv;
};
-static void check_one_mergetag(struct commit *commit,
+static int check_one_mergetag(struct commit *commit,
struct commit_extra_header *extra,
void *data)
{
hash_object_file(extra->value, extra->len, type_name(OBJ_TAG), &tag_oid);
tag = lookup_tag(&tag_oid);
if (!tag)
- die(_("bad mergetag in commit '%s'"), ref);
+ return error(_("bad mergetag in commit '%s'"), ref);
if (parse_tag_buffer(tag, extra->value, extra->len))
- die(_("malformed mergetag in commit '%s'"), ref);
+ return error(_("malformed mergetag in commit '%s'"), ref);
/* iterate over new parents */
for (i = 1; i < mergetag_data->argc; i++) {
struct object_id oid;
if (get_oid(mergetag_data->argv[i], &oid) < 0)
- die(_("Not a valid object name: '%s'"), mergetag_data->argv[i]);
+ return error(_("Not a valid object name: '%s'"),
+ mergetag_data->argv[i]);
if (!oidcmp(&tag->tagged->oid, &oid))
- return; /* found */
+ return 0; /* found */
}
- die(_("original commit '%s' contains mergetag '%s' that is discarded; "
- "use --edit instead of --graft"), ref, oid_to_hex(&tag_oid));
+ return error(_("original commit '%s' contains mergetag '%s' that is "
+ "discarded; use --edit instead of --graft"), ref,
+ oid_to_hex(&tag_oid));
}
-static void check_mergetags(struct commit *commit, int argc, const char **argv)
+static int check_mergetags(struct commit *commit, int argc, const char **argv)
{
struct check_mergetag_data mergetag_data;
mergetag_data.argc = argc;
mergetag_data.argv = argv;
- for_each_mergetag(check_one_mergetag, commit, &mergetag_data);
+ return for_each_mergetag(check_one_mergetag, commit, &mergetag_data);
}
-static int create_graft(int argc, const char **argv, int force)
+static int create_graft(int argc, const char **argv, int force, int gentle)
{
struct object_id old_oid, new_oid;
const char *old_ref = argv[0];
unsigned long size;
if (get_oid(old_ref, &old_oid) < 0)
- die(_("Not a valid object name: '%s'"), old_ref);
- commit = lookup_commit_or_die(&old_oid, old_ref);
+ return error(_("Not a valid object name: '%s'"), old_ref);
+ commit = lookup_commit_reference(&old_oid);
+ if (!commit)
+ return error(_("could not parse %s"), old_ref);
buffer = get_commit_buffer(commit, &size);
strbuf_add(&buf, buffer, size);
unuse_commit_buffer(commit, buffer);
- replace_parents(&buf, argc - 1, &argv[1]);
+ if (replace_parents(&buf, argc - 1, &argv[1]) < 0) {
+ strbuf_release(&buf);
+ return -1;
+ }
if (remove_signature(&buf)) {
warning(_("the original commit '%s' has a gpg signature."), old_ref);
warning(_("the signature will be removed in the replacement commit!"));
}
- check_mergetags(commit, argc, argv);
+ if (check_mergetags(commit, argc, argv)) {
+ strbuf_release(&buf);
+ return -1;
+ }
- if (write_object_file(buf.buf, buf.len, commit_type, &new_oid))
- die(_("could not write replacement commit for: '%s'"), old_ref);
+ if (write_object_file(buf.buf, buf.len, commit_type, &new_oid)) {
+ strbuf_release(&buf);
+ return error(_("could not write replacement commit for: '%s'"),
+ old_ref);
+ }
strbuf_release(&buf);
- if (!oidcmp(&old_oid, &new_oid))
+ if (!oidcmp(&old_oid, &new_oid)) {
+ if (gentle) {
+ warning("graft for '%s' unnecessary", oid_to_hex(&old_oid));
+ return 0;
+ }
return error("new commit is the same as the old one: '%s'", oid_to_hex(&old_oid));
+ }
return replace_object_oid(old_ref, &old_oid, "replacement", &new_oid, force);
}
+static int convert_graft_file(int force)
+{
+ const char *graft_file = get_graft_file();
+ FILE *fp = fopen_or_warn(graft_file, "r");
+ struct strbuf buf = STRBUF_INIT, err = STRBUF_INIT;
+ struct argv_array args = ARGV_ARRAY_INIT;
+
+ if (!fp)
+ return -1;
+
+ while (strbuf_getline(&buf, fp) != EOF) {
+ if (*buf.buf == '#')
+ continue;
+
+ argv_array_split(&args, buf.buf);
+ if (args.argc && create_graft(args.argc, args.argv, force, 1))
+ strbuf_addf(&err, "\n\t%s", buf.buf);
+ argv_array_clear(&args);
+ }
+ fclose(fp);
+
+ strbuf_release(&buf);
+
+ if (!err.len)
+ return unlink_or_warn(graft_file);
+
+ warning(_("could not convert the following graft(s):\n%s"), err.buf);
+ strbuf_release(&err);
+
+ return -1;
+}
+
int cmd_replace(int argc, const char **argv, const char *prefix)
{
int force = 0;
MODE_DELETE,
MODE_EDIT,
MODE_GRAFT,
+ MODE_CONVERT_GRAFT_FILE,
MODE_REPLACE
} cmdmode = MODE_UNSPECIFIED;
struct option options[] = {
OPT_CMDMODE('d', "delete", &cmdmode, N_("delete replace refs"), MODE_DELETE),
OPT_CMDMODE('e', "edit", &cmdmode, N_("edit existing object"), MODE_EDIT),
OPT_CMDMODE('g', "graft", &cmdmode, N_("change a commit's parents"), MODE_GRAFT),
+ OPT_CMDMODE(0, "convert-graft-file", &cmdmode, N_("convert existing graft file"), MODE_CONVERT_GRAFT_FILE),
OPT_BOOL_F('f', "force", &force, N_("replace the ref if it exists"),
PARSE_OPT_NOCOMPLETE),
OPT_BOOL(0, "raw", &raw, N_("do not pretty-print contents for --edit")),
if (force &&
cmdmode != MODE_REPLACE &&
cmdmode != MODE_EDIT &&
- cmdmode != MODE_GRAFT)
+ cmdmode != MODE_GRAFT &&
+ cmdmode != MODE_CONVERT_GRAFT_FILE)
usage_msg_opt("-f only makes sense when writing a replacement",
git_replace_usage, options);
if (argc < 1)
usage_msg_opt("-g needs at least one argument",
git_replace_usage, options);
- return create_graft(argc, argv, force);
+ return create_graft(argc, argv, force, 0);
+
+ case MODE_CONVERT_GRAFT_FILE:
+ if (argc != 0)
+ usage_msg_opt("--convert-graft-file takes no argument",
+ git_replace_usage, options);
+ return !!convert_graft_file(force);
case MODE_LIST:
if (argc > 1)
return list_replace_refs(argv[0], format);
default:
- die("BUG: invalid cmdmode %d", (int)cmdmode);
+ BUG("invalid cmdmode %d", (int)cmdmode);
}
}
unborn = !strcmp(rev, "HEAD") && get_oid("HEAD", &oid);
if (unborn) {
/* reset on unborn branch: treat as reset to empty tree */
- hashcpy(oid.hash, EMPTY_TREE_SHA1_BIN);
+ oidcpy(&oid, the_hash_algo->empty_tree);
} else if (!pathspec.nr) {
struct commit *commit;
if (get_oid_committish(rev, &oid))
if (read_cache() < 0)
die(_("Could not read the index"));
if (the_index.split_index) {
- const unsigned char *sha1 = the_index.split_index->base_sha1;
- const char *path = git_path("sharedindex.%s", sha1_to_hex(sha1));
+ const struct object_id *oid = &the_index.split_index->base_oid;
+ const char *path = git_path("sharedindex.%s", oid_to_hex(oid));
strbuf_reset(&buf);
puts(relative_path(path, prefix, &buf));
}
return errs;
}
-static struct lock_file lock_file;
-
static int show_only = 0, force = 0, index_only = 0, recursive = 0, quiet = 0;
static int ignore_unmatch = 0;
int cmd_rm(int argc, const char **argv, const char *prefix)
{
+ struct lock_file lock_file = LOCK_INIT;
int i;
struct pathspec pathspec;
char *seen;
int cmd_send_pack(int argc, const char **argv, const char *prefix)
{
- int i, nr_refspecs = 0;
- const char **refspecs = NULL;
+ struct refspec rs = REFSPEC_INIT_PUSH;
const char *remote_name = NULL;
struct remote *remote = NULL;
const char *dest = NULL;
argc = parse_options(argc, argv, prefix, options, send_pack_usage, 0);
if (argc > 0) {
dest = argv[0];
- refspecs = (const char **)(argv + 1);
- nr_refspecs = argc - 1;
+ refspec_appendn(&rs, argv + 1, argc - 1);
}
if (!dest)
args.push_options = push_options.nr ? &push_options : NULL;
if (from_stdin) {
- struct argv_array all_refspecs = ARGV_ARRAY_INIT;
-
- for (i = 0; i < nr_refspecs; i++)
- argv_array_push(&all_refspecs, refspecs[i]);
-
if (args.stateless_rpc) {
const char *buf;
while ((buf = packet_read_line(0, NULL)))
- argv_array_push(&all_refspecs, buf);
+ refspec_append(&rs, buf);
} else {
struct strbuf line = STRBUF_INIT;
while (strbuf_getline(&line, stdin) != EOF)
- argv_array_push(&all_refspecs, line.buf);
+ refspec_append(&rs, line.buf);
strbuf_release(&line);
}
-
- refspecs = all_refspecs.argv;
- nr_refspecs = all_refspecs.argc;
}
/*
* --all and --mirror are incompatible; neither makes sense
* with any refspecs.
*/
- if ((nr_refspecs > 0 && (send_all || args.send_mirror)) ||
+ if ((rs.nr > 0 && (send_all || args.send_mirror)) ||
(send_all && args.send_mirror))
usage_with_options(send_pack_usage, options);
BUG("unknown protocol version");
}
- transport_verify_remote_names(nr_refspecs, refspecs);
-
local_refs = get_local_heads();
flags = MATCH_REFS_NONE;
flags |= MATCH_REFS_MIRROR;
/* match them up */
- if (match_push_refs(local_refs, &remote_refs, nr_refspecs, refspecs, flags))
+ if (match_push_refs(local_refs, &remote_refs, &rs, flags))
return -1;
if (!is_empty_cas(&cas))
#include "run-command.h"
#include "remote.h"
#include "refs.h"
+#include "refspec.h"
#include "connect.h"
#include "revision.h"
#include "diffcore.h"
}
static int clone_submodule(const char *path, const char *gitdir, const char *url,
- const char *depth, struct string_list *reference,
+ const char *depth, struct string_list *reference, int dissociate,
int quiet, int progress)
{
struct child_process cp = CHILD_PROCESS_INIT;
argv_array_pushl(&cp.args, "--reference",
item->string, NULL);
}
+ if (dissociate)
+ argv_array_push(&cp.args, "--dissociate");
if (gitdir && *gitdir)
argv_array_pushl(&cp.args, "--separate-git-dir", gitdir, NULL);
char *p, *path = NULL, *sm_gitdir;
struct strbuf sb = STRBUF_INIT;
struct string_list reference = STRING_LIST_INIT_NODUP;
+ int dissociate = 0;
char *sm_alternate = NULL, *error_strategy = NULL;
struct option module_clone_options[] = {
OPT_STRING_LIST(0, "reference", &reference,
N_("repo"),
N_("reference repository")),
+ OPT_BOOL(0, "dissociate", &dissociate,
+ N_("use --reference only while cloning")),
OPT_STRING(0, "depth", &depth,
N_("string"),
N_("depth for shallow clones")),
prepare_possible_alternates(name, &reference);
- if (clone_submodule(path, sm_gitdir, url, depth, &reference,
+ if (clone_submodule(path, sm_gitdir, url, depth, &reference, dissociate,
quiet, progress))
die(_("clone of '%s' into submodule path '%s' failed"),
url, path);
int quiet;
int recommend_shallow;
struct string_list references;
+ int dissociate;
const char *depth;
const char *recursive_prefix;
const char *prefix;
int failed_clones_nr, failed_clones_alloc;
};
#define SUBMODULE_UPDATE_CLONE_INIT {0, MODULE_LIST_INIT, 0, \
- SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, \
+ SUBMODULE_UPDATE_STRATEGY_INIT, 0, 0, -1, STRING_LIST_INIT_DUP, 0, \
NULL, NULL, NULL, \
STRING_LIST_INIT_DUP, 0, NULL, 0, 0}
for_each_string_list_item(item, &suc->references)
argv_array_pushl(&child->args, "--reference", item->string, NULL);
}
+ if (suc->dissociate)
+ argv_array_push(&child->args, "--dissociate");
if (suc->depth)
argv_array_push(&child->args, suc->depth);
N_("rebase, merge, checkout or none")),
OPT_STRING_LIST(0, "reference", &suc.references, N_("repo"),
N_("reference repository")),
+ OPT_BOOL(0, "dissociate", &suc.dissociate,
+ N_("use --reference only while cloning")),
OPT_STRING(0, "depth", &suc.depth, "<depth>",
N_("Create a shallow clone truncated to the "
"specified number of revisions")),
/* Check the refspec */
if (argc > 2) {
- int i, refspec_nr = argc - 2;
+ int i;
struct ref *local_refs = get_local_heads();
- struct refspec *refspec = parse_push_refspec(refspec_nr,
- argv + 2);
+ struct refspec refspec = REFSPEC_INIT_PUSH;
- for (i = 0; i < refspec_nr; i++) {
- struct refspec *rs = refspec + i;
+ refspec_appendn(&refspec, argv + 2, argc - 2);
+
+ for (i = 0; i < refspec.nr; i++) {
+ const struct refspec_item *rs = &refspec.items[i];
if (rs->pattern || rs->matching)
continue;
rs->src);
}
}
- free_refspec(refspec_nr, refspec);
+ refspec_clear(&refspec);
}
free(head);
return !is_submodule_active(the_repository, argv[1]);
}
+/*
+ * Exit non-zero if any of the submodule names given on the command line is
+ * invalid. If no names are given, filter stdin to print only valid names
+ * (which is primarily intended for testing).
+ */
+static int check_name(int argc, const char **argv, const char *prefix)
+{
+ if (argc > 1) {
+ while (*++argv) {
+ if (check_submodule_name(*argv) < 0)
+ return 1;
+ }
+ } else {
+ struct strbuf buf = STRBUF_INIT;
+ while (strbuf_getline(&buf, stdin) != EOF) {
+ if (!check_submodule_name(buf.buf))
+ printf("%s\n", buf.buf);
+ }
+ strbuf_release(&buf);
+ }
+ return 0;
+}
+
#define SUPPORT_SUPER_PREFIX (1<<0)
struct cmd_struct {
{"push-check", push_check, 0},
{"absorb-git-dirs", absorb_git_dirs, SUPPORT_SUPER_PREFIX},
{"is-active", is_active, 0},
+ {"check-name", check_name, 0},
};
int cmd_submodule__helper(int argc, const char **argv, const char *prefix)
struct strbuf header = STRBUF_INIT;
char *path = NULL;
- type = oid_object_info(object, NULL);
+ type = oid_object_info(the_repository, object, NULL);
if (type <= OBJ_NONE)
die(_("bad object type."));
}
strbuf_addstr(sb, " (");
- type = oid_object_info(oid, NULL);
+ type = oid_object_info(the_repository, oid, NULL);
switch (type) {
default:
strbuf_addstr(sb, "object of unknown type");
if (!(obj->flags & FLAG_OPEN)) {
unsigned long size;
- int type = oid_object_info(&obj->oid, &size);
+ int type = oid_object_info(the_repository, &obj->oid, &size);
if (type != obj->type || type <= 0)
die("object of unexpected type");
obj->flags |= FLAG_WRITTEN;
if (!obj_buf)
die("Whoops! Cannot find object '%s'", oid_to_hex(&obj->oid));
if (fsck_object(obj, obj_buf->buffer, obj_buf->size, &fsck_options))
- die("Error in object");
+ die("fsck error in packed object");
fsck_options.walk = check_object;
if (fsck_walk(obj, NULL, &fsck_options))
die("Error on reachable objects of %s", oid_to_hex(&obj->oid));
unpack_all();
the_hash_algo->update_fn(&ctx, buffer, offset);
the_hash_algo->final_fn(oid.hash, &ctx);
- if (strict)
+ if (strict) {
write_rest();
+ if (fsck_finish(&fsck_options))
+ die(_("fsck error in pack objects"));
+ }
if (hashcmp(fill(the_hash_algo->rawsz), oid.hash))
die("final sha1 did not match");
use(the_hash_algo->rawsz);
return error("%s: is a directory - add files inside instead", path);
}
-static int process_path(const char *path)
+static int process_path(const char *path, struct stat *st, int stat_errno)
{
int pos, len;
- struct stat st;
const struct cache_entry *ce;
len = strlen(path);
* First things first: get the stat information, to decide
* what to do about the pathname!
*/
- if (lstat(path, &st) < 0)
- return process_lstat_error(path, errno);
+ if (stat_errno)
+ return process_lstat_error(path, stat_errno);
- if (S_ISDIR(st.st_mode))
- return process_directory(path, len, &st);
+ if (S_ISDIR(st->st_mode))
+ return process_directory(path, len, st);
- return add_one_path(ce, path, len, &st);
+ return add_one_path(ce, path, len, st);
}
static int add_cacheinfo(unsigned int mode, const struct object_id *oid,
int size, len, option;
struct cache_entry *ce;
- if (!verify_path(path))
+ if (!verify_path(path, mode))
return error("Invalid path '%s'", path);
len = strlen(path);
static void update_one(const char *path)
{
- if (!verify_path(path)) {
+ int stat_errno = 0;
+ struct stat st;
+
+ if (mark_valid_only || mark_skip_worktree_only || force_remove ||
+ mark_fsmonitor_only)
+ st.st_mode = 0;
+ else if (lstat(path, &st) < 0) {
+ st.st_mode = 0;
+ stat_errno = errno;
+ } /* else stat is valid */
+
+ if (!verify_path(path, st.st_mode)) {
fprintf(stderr, "Ignoring path %s\n", path);
return;
}
report("remove '%s'", path);
return;
}
- if (process_path(path))
+ if (process_path(path, &st, stat_errno))
die("Unable to process path %s", path);
report("add '%s'", path);
}
path_name = uq.buf;
}
- if (!verify_path(path_name)) {
+ if (!verify_path(path_name, mode)) {
fprintf(stderr, "Ignoring path %s\n", path_name);
continue;
}
report(_("Untracked cache enabled for '%s'"), get_git_work_tree());
break;
default:
- die("BUG: bad untracked_cache value: %d", untracked_cache);
+ BUG("bad untracked_cache value: %d", untracked_cache);
}
if (fsmonitor > 0) {
int detach;
int checkout;
int keep_locked;
- const char *new_branch;
- int force_new_branch;
};
static int show_only;
strbuf_addf(&sb, "%s/commondir", sb_repo.buf);
write_file(sb.buf, "../..");
- fprintf_ln(stderr, _("Preparing %s (identifier %s)"), path, name);
-
argv_array_pushf(&child_env, "%s=%s", GIT_DIR_ENVIRONMENT, sb_git.buf);
argv_array_pushf(&child_env, "%s=%s", GIT_WORK_TREE_ENVIRONMENT, path);
cp.git_cmd = 1;
return ret;
}
+static void print_preparing_worktree_line(int detach,
+ const char *branch,
+ const char *new_branch,
+ int force_new_branch)
+{
+ if (force_new_branch) {
+ struct commit *commit = lookup_commit_reference_by_name(new_branch);
+ if (!commit)
+ printf_ln(_("Preparing worktree (new branch '%s')"), new_branch);
+ else
+ printf_ln(_("Preparing worktree (resetting branch '%s'; was at %s)"),
+ new_branch,
+ find_unique_abbrev(&commit->object.oid, DEFAULT_ABBREV));
+ } else if (new_branch) {
+ printf_ln(_("Preparing worktree (new branch '%s')"), new_branch);
+ } else {
+ struct strbuf s = STRBUF_INIT;
+ if (!detach && !strbuf_check_branch_ref(&s, branch) &&
+ ref_exists(s.buf))
+ printf_ln(_("Preparing worktree (checking out '%s')"),
+ branch);
+ else {
+ struct commit *commit = lookup_commit_reference_by_name(branch);
+ if (!commit)
+ die(_("invalid reference: %s"), branch);
+ printf_ln(_("Preparing worktree (detached HEAD %s)"),
+ find_unique_abbrev(&commit->object.oid, DEFAULT_ABBREV));
+ }
+ strbuf_release(&s);
+ }
+}
+
+static const char *dwim_branch(const char *path, const char **new_branch)
+{
+ int n;
+ const char *s = worktree_basename(path, &n);
+ const char *branchname = xstrndup(s, n);
+ struct strbuf ref = STRBUF_INIT;
+
+ UNLEAK(branchname);
+ if (!strbuf_check_branch_ref(&ref, branchname) &&
+ ref_exists(ref.buf)) {
+ strbuf_release(&ref);
+ return branchname;
+ }
+
+ *new_branch = branchname;
+ if (guess_remote) {
+ struct object_id oid;
+ const char *remote =
+ unique_tracking_name(*new_branch, &oid);
+ return remote;
+ }
+ return NULL;
+}
+
static int add(int ac, const char **av, const char *prefix)
{
struct add_opts opts;
const char *new_branch_force = NULL;
char *path;
const char *branch;
+ const char *new_branch = NULL;
const char *opt_track = NULL;
struct option options[] = {
OPT__FORCE(&opts.force,
N_("checkout <branch> even if already checked out in other worktree"),
PARSE_OPT_NOCOMPLETE),
- OPT_STRING('b', NULL, &opts.new_branch, N_("branch"),
+ OPT_STRING('b', NULL, &new_branch, N_("branch"),
N_("create a new branch")),
OPT_STRING('B', NULL, &new_branch_force, N_("branch"),
N_("create or reset a branch")),
memset(&opts, 0, sizeof(opts));
opts.checkout = 1;
ac = parse_options(ac, av, prefix, options, worktree_usage, 0);
- if (!!opts.detach + !!opts.new_branch + !!new_branch_force > 1)
+ if (!!opts.detach + !!new_branch + !!new_branch_force > 1)
die(_("-b, -B, and --detach are mutually exclusive"));
if (ac < 1 || ac > 2)
usage_with_options(worktree_usage, options);
if (!strcmp(branch, "-"))
branch = "@{-1}";
- opts.force_new_branch = !!new_branch_force;
- if (opts.force_new_branch) {
+ if (new_branch_force) {
struct strbuf symref = STRBUF_INIT;
- opts.new_branch = new_branch_force;
+ new_branch = new_branch_force;
if (!opts.force &&
- !strbuf_check_branch_ref(&symref, opts.new_branch) &&
+ !strbuf_check_branch_ref(&symref, new_branch) &&
ref_exists(symref.buf))
die_if_checked_out(symref.buf, 0);
strbuf_release(&symref);
}
- if (ac < 2 && !opts.new_branch && !opts.detach) {
- int n;
- const char *s = worktree_basename(path, &n);
- opts.new_branch = xstrndup(s, n);
- if (guess_remote) {
- struct object_id oid;
- const char *remote =
- unique_tracking_name(opts.new_branch, &oid);
- if (remote)
- branch = remote;
- }
+ if (ac < 2 && !new_branch && !opts.detach) {
+ const char *s = dwim_branch(path, &new_branch);
+ if (s)
+ branch = s;
}
- if (ac == 2 && !opts.new_branch && !opts.detach) {
+ if (ac == 2 && !new_branch && !opts.detach) {
struct object_id oid;
struct commit *commit;
const char *remote;
if (!commit) {
remote = unique_tracking_name(branch, &oid);
if (remote) {
- opts.new_branch = branch;
+ new_branch = branch;
branch = remote;
}
}
}
- if (opts.new_branch) {
+ print_preparing_worktree_line(opts.detach, branch, new_branch, !!new_branch_force);
+
+ if (new_branch) {
struct child_process cp = CHILD_PROCESS_INIT;
cp.git_cmd = 1;
argv_array_push(&cp.args, "branch");
- if (opts.force_new_branch)
+ if (new_branch_force)
argv_array_push(&cp.args, "--force");
- argv_array_push(&cp.args, opts.new_branch);
+ argv_array_push(&cp.args, new_branch);
argv_array_push(&cp.args, branch);
if (opt_track)
argv_array_push(&cp.args, opt_track);
if (run_command(&cp))
return -1;
- branch = opts.new_branch;
+ branch = new_branch;
} else if (opt_track) {
die(_("--[no-]track can only be used if a new branch is created"));
}
* pack, and write into it.
*/
if (!idx)
- die("BUG: should not happen");
+ BUG("should not happen");
hashfile_truncate(state->f, &checkpoint);
state->offset = checkpoint.offset;
finish_bulk_checkin(state);
int create_bundle(struct bundle_header *header, const char *path,
int argc, const char **argv)
{
- static struct lock_file lock;
+ struct lock_file lock = LOCK_INIT;
int bundle_fd = -1;
int bundle_to_stdout;
int ref_count = 0;
/*
* "sub" can be an empty tree if all subentries are i-t-a.
*/
- if (contains_ita && !oidcmp(oid, &empty_tree_oid))
+ if (contains_ita && is_empty_tree_oid(oid))
continue;
strbuf_grow(&buffer, entlen + 100);
if (0 <= it->entry_count) {
if (size < rawsz)
goto free_return;
- memcpy(it->oid.hash, (const unsigned char*)buf, rawsz);
+ oidread(&it->oid, (const unsigned char *)buf);
buf += rawsz;
size -= rawsz;
}
drop_cache_tree : 1;
struct hashmap name_hash;
struct hashmap dir_hash;
- unsigned char sha1[20];
+ struct object_id oid;
struct untracked_cache *untracked;
uint64_t fsmonitor_last_update;
struct ewah_bitmap *fsmonitor_dirty;
#define read_blob_data_from_cache(path, sz) read_blob_data_from_index(&the_index, (path), (sz))
#endif
+#define TYPE_BITS 3
+
+/*
+ * Values in this enum (except those outside the 3 bit range) are part
+ * of pack file format. See Documentation/technical/pack-format.txt
+ * for more information.
+ */
enum object_type {
OBJ_BAD = -1,
OBJ_NONE = 0,
*/
extern int index_has_changes(struct strbuf *sb);
-extern int verify_path(const char *path);
+extern int verify_path(const char *path, unsigned mode);
extern int strcmp_offset(const char *s1, const char *s2, size_t *first_change);
extern int index_dir_exists(struct index_state *istate, const char *name, int namelen);
extern void adjust_dirname_case(struct index_state *istate, char *name);
memset(oid->hash, 0, GIT_MAX_RAWSZ);
}
-
-#define EMPTY_TREE_SHA1_HEX \
- "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
-#define EMPTY_TREE_SHA1_BIN_LITERAL \
- "\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60" \
- "\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04"
-extern const struct object_id empty_tree_oid;
-#define EMPTY_TREE_SHA1_BIN (empty_tree_oid.hash)
-
-#define EMPTY_BLOB_SHA1_HEX \
- "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"
-#define EMPTY_BLOB_SHA1_BIN_LITERAL \
- "\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b" \
- "\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91"
-extern const struct object_id empty_blob_oid;
+static inline void oidread(struct object_id *oid, const unsigned char *hash)
+{
+ memcpy(oid->hash, hash, the_hash_algo->rawsz);
+}
static inline int is_empty_blob_sha1(const unsigned char *sha1)
{
return !oidcmp(oid, the_hash_algo->empty_tree);
}
+const char *empty_tree_oid_hex(void);
+const char *empty_blob_oid_hex(void);
+
/* set default permissions by passing mode arguments to open(2) */
int git_mkstemps_mode(char *pattern, int suffix_len, int mode);
int git_mkstemp_mode(char *pattern, int mode);
int longest_ancestor_length(const char *path, struct string_list *prefixes);
char *strip_path_suffix(const char *path, const char *suffix);
int daemon_avoid_alias(const char *path);
-extern int is_ntfs_dotgit(const char *name);
+
+/*
+ * These functions match their is_hfs_dotgit() counterparts; see utf8.h for
+ * details.
+ */
+int is_ntfs_dotgit(const char *name);
+int is_ntfs_dotgitmodules(const char *name);
+int is_ntfs_dotgitignore(const char *name);
+int is_ntfs_dotgitattributes(const char *name);
/*
* Returns true iff "str" could be confused as a command-line option when
}
/* Read and unpack an object file into memory, write memory to an object file */
-extern int oid_object_info(const struct object_id *, unsigned long *);
+int oid_object_info(struct repository *r, const struct object_id *, unsigned long *);
extern int hash_object_file(const void *buf, unsigned long len,
const char *type, struct object_id *oid);
* with the specified name. This function does not respect replace
* references.
*/
-extern int has_loose_object_nonlocal(const unsigned char *sha1);
+extern int has_loose_object_nonlocal(const struct object_id *oid);
extern void assert_oid_type(const struct object_id *oid, enum object_type expect);
#define FALLBACK_DEFAULT_ABBREV 7
struct object_context {
- unsigned char tree[20];
unsigned mode;
/*
* symlink_path is only used by get_tree_entry_follow_symlinks,
struct pack_entry {
off_t offset;
- unsigned char sha1[20];
struct packed_git *p;
};
#define OBJECT_INFO_QUICK 8
/* Do not check loose object */
#define OBJECT_INFO_IGNORE_LOOSE 16
-extern int oid_object_info_extended(const struct object_id *, struct object_info *, unsigned flags);
+
+int oid_object_info_extended(struct repository *r,
+ const struct object_id *,
+ struct object_info *, unsigned flags);
/*
* Set this to 0 to prevent sha1_object_info_extended() from fetching missing
#include "cache.h"
#include "remote.h"
+#include "refspec.h"
#include "checkout.h"
struct tracking_name_data {
static int check_tracking_name(struct remote *remote, void *cb_data)
{
struct tracking_name_data *cb = cb_data;
- struct refspec query;
- memset(&query, 0, sizeof(struct refspec));
+ struct refspec_item query;
+ memset(&query, 0, sizeof(struct refspec_item));
query.src = cb->src_ref;
if (remote_find_tracking(remote, &query) ||
get_oid(query.dst, cb->dst_oid)) {
export GIT_PROVE_OPTS="--timer --jobs 3 --state=failed,slow,save"
export GIT_TEST_OPTS="--verbose-log -x"
export GIT_TEST_CLONE_2GB=YesPlease
+if [ "$jobname" = linux-gcc ]; then
+ export CC=gcc-8
+fi
case "$jobname" in
linux-clang|linux-gcc)
make --quiet test
if test "$jobname" = "linux-gcc"
then
- GIT_TEST_SPLIT_INDEX=YesPlease make --quiet test
+ export GIT_TEST_SPLIT_INDEX=yes
+ export GIT_TEST_FULL_IN_PACK_ARRAY=true
+ export GIT_TEST_OE_SIZE=10
+ make --quiet test
fi
check_unignored_build_artifacts
break;
case COLOR_ANSI:
if (len < 2)
- die("BUG: color parsing ran out of space");
+ BUG("color parsing ran out of space");
*out++ = type;
*out++ = '0' + c->value;
break;
#undef OUT
#define OUT(x) do { \
if (dst == end) \
- die("BUG: color parsing ran out of space"); \
+ BUG("color parsing ran out of space"); \
*dst++ = (x); \
} while(0)
display_table(list, colopts, &nopts);
break;
default:
- die("BUG: invalid layout mode %d", COL_LAYOUT(colopts));
+ BUG("invalid layout mode %d", COL_LAYOUT(colopts));
}
}
static int fill_commit_in_graph(struct commit *item, struct commit_graph *g, uint32_t pos)
{
- struct object_id oid;
uint32_t edge_value;
uint32_t *parent_data_ptr;
uint64_t date_low, date_high;
item->object.parsed = 1;
item->graph_pos = pos;
- hashcpy(oid.hash, commit_data);
- item->tree = lookup_tree(&oid);
+ item->maybe_tree = NULL;
date_high = get_be32(commit_data + g->hash_len + 8) & 0x3;
date_low = get_be32(commit_data + g->hash_len + 12);
return 0;
}
+static struct tree *load_tree_for_commit(struct commit_graph *g, struct commit *c)
+{
+ struct object_id oid;
+ const unsigned char *commit_data = g->chunk_commit_data +
+ GRAPH_DATA_WIDTH * (c->graph_pos);
+
+ hashcpy(oid.hash, commit_data);
+ c->maybe_tree = lookup_tree(&oid);
+
+ return c->maybe_tree;
+}
+
+struct tree *get_commit_tree_in_graph(const struct commit *c)
+{
+ if (c->maybe_tree)
+ return c->maybe_tree;
+ if (c->graph_pos == COMMIT_NOT_FROM_GRAPH)
+ BUG("get_commit_tree_in_graph called from non-commit-graph commit");
+
+ return load_tree_for_commit(commit_graph, (struct commit *)c);
+}
+
static void write_graph_chunk_fanout(struct hashfile *f,
struct commit **commits,
int nr_commits)
uint32_t packedDate[2];
parse_commit(*list);
- hashwrite(f, (*list)->tree->object.oid.hash, hash_len);
+ hashwrite(f, get_commit_tree_oid(*list)->hash, hash_len);
parent = (*list)->parents;
struct object_info oi = OBJECT_INFO_INIT;
oi.typep = &type;
- if (packed_object_info(pack, offset, &oi) < 0)
+ if (packed_object_info(the_repository, pack, offset, &oi) < 0)
die("unable to get type of object %s", oid_to_hex(oid));
if (type != OBJ_COMMIT)
*/
int parse_commit_in_graph(struct commit *item);
+struct tree *get_commit_tree_in_graph(const struct commit *c);
+
struct commit_graph {
int graph_fd;
#include "prio-queue.h"
#include "sha1-lookup.h"
#include "wt-status.h"
+#include "advice.h"
static struct commit_extra_header *read_commit_extra_header_lines(const char *buf, size_t len, const char **);
struct strbuf buf = STRBUF_INIT;
if (!fp)
return -1;
+ if (advice_graft_file_deprecated)
+ advise(_("Support for <GIT_DIR>/info/grafts is deprecated\n"
+ "and will be removed in a future Git version.\n"
+ "\n"
+ "Please use \"git replace --convert-graft-file\"\n"
+ "to convert the grafts into replace refs.\n"
+ "\n"
+ "Turn this message off by running\n"
+ "\"git config advice.graftFileDeprecated false\""));
while (!strbuf_getwholeline(&buf, fp, '\n')) {
/* The format is just "Commit Parent1 Parent2 ...\n" */
struct commit_graft *graft = read_graft_line(&buf);
}
}
+struct tree *get_commit_tree(const struct commit *commit)
+{
+ if (commit->maybe_tree || !commit->object.parsed)
+ return commit->maybe_tree;
+
+ if (commit->graph_pos == COMMIT_NOT_FROM_GRAPH)
+ BUG("commit has NULL tree, but was not loaded from commit-graph");
+
+ return get_commit_tree_in_graph(commit);
+}
+
+struct object_id *get_commit_tree_oid(const struct commit *commit)
+{
+ return &get_commit_tree(commit)->object.oid;
+}
+
const void *detach_commit_buffer(struct commit *commit, unsigned long *sizep)
{
struct commit_buffer *v = buffer_slab_peek(&buffer_slab, commit);
if (tail <= bufptr + tree_entry_len + 1 || memcmp(bufptr, "tree ", 5) ||
bufptr[tree_entry_len] != '\n')
return error("bogus commit object %s", oid_to_hex(&item->object.oid));
- if (get_sha1_hex(bufptr + 5, parent.hash) < 0)
+ if (get_oid_hex(bufptr + 5, &parent) < 0)
return error("bad tree pointer in commit %s",
oid_to_hex(&item->object.oid));
- item->tree = lookup_tree(&parent);
+ item->maybe_tree = lookup_tree(&parent);
bufptr += tree_entry_len + 1; /* "tree " + "hex sha1" + "\n" */
pptr = &item->parents;
struct commit *new_parent;
if (tail <= bufptr + parent_entry_len + 1 ||
- get_sha1_hex(bufptr + 7, parent.hash) ||
+ get_oid_hex(bufptr + 7, &parent) ||
bufptr[parent_entry_len] != '\n')
return error("bad parents in commit %s", oid_to_hex(&item->object.oid));
bufptr += parent_entry_len + 1;
return extra;
}
-void for_each_mergetag(each_mergetag_fn fn, struct commit *commit, void *data)
+int for_each_mergetag(each_mergetag_fn fn, struct commit *commit, void *data)
{
struct commit_extra_header *extra, *to_free;
+ int res = 0;
to_free = read_commit_extra_headers(commit, NULL);
- for (extra = to_free; extra; extra = extra->next) {
+ for (extra = to_free; !res && extra; extra = extra->next) {
if (strcmp(extra->key, "mergetag"))
continue; /* not a merge tag */
- fn(commit, extra, data);
+ res = fn(commit, extra, data);
}
free_commit_extra_headers(to_free);
+ return res;
}
static inline int standard_header_field(const char *field, size_t len)
struct commit {
struct object object;
void *util;
- unsigned int index;
timestamp_t date;
struct commit_list *parents;
- struct tree *tree;
+
+ /*
+ * If the commit is loaded from the commit-graph file, then this
+ * member may be NULL. Only access it through get_commit_tree()
+ * or get_commit_tree_oid().
+ */
+ struct tree *maybe_tree;
uint32_t graph_pos;
+ unsigned int index;
};
extern int save_commit_buffer;
*/
void free_commit_buffer(struct commit *);
+struct tree *get_commit_tree(const struct commit *);
+struct object_id *get_commit_tree_oid(const struct commit *);
+
/*
* Disassociate any cached object buffer from the commit, but do not free it.
* The buffer (or NULL, if none) is returned.
/* Find the end of the log message, the right place for a new trailer. */
extern int ignore_non_trailer(const char *buf, size_t len);
-typedef void (*each_mergetag_fn)(struct commit *commit, struct commit_extra_header *extra,
+typedef int (*each_mergetag_fn)(struct commit *commit, struct commit_extra_header *extra,
void *cb_data);
-extern void for_each_mergetag(each_mergetag_fn fn, struct commit *commit, void *data);
+extern int for_each_mergetag(each_mergetag_fn fn, struct commit *commit, void *data);
struct merge_remote_desc {
struct object *obj; /* the named object, could be a tag */
if (conf->u.buf.pos > 0) {
conf->u.buf.pos--;
if (conf->u.buf.buf[conf->u.buf.pos] != c)
- die("BUG: config_buf can only ungetc the same character");
+ BUG("config_buf can only ungetc the same character");
return c;
}
strbuf_realpath(&path, cf->path, 1);
slash = find_last_dir_sep(path.buf);
if (!slash)
- die("BUG: how is this possible?");
+ BUG("how is this possible?");
strbuf_splice(pat, 0, 1, path.buf, slash - path.buf);
prefix = slash - path.buf + 1 /* slash */;
} else if (!is_absolute_path(pat->buf))
l_item->value_index = e->value_list.nr - 1;
if (!cf)
- die("BUG: configset_add_value has no source");
+ BUG("configset_add_value has no source");
if (cf->name) {
kv_info->filename = strintern(cf->name);
kv_info->linenr = cf->linenr;
if (type == CONFIG_EVENT_SECTION) {
if (cf->var.len < 2 || cf->var.buf[cf->var.len - 1] != '.')
- BUG("Invalid section name '%s'", cf->var.buf);
+ return error("invalid section name '%s'", cf->var.buf);
/* Is this the section we were looking for? */
store->is_keys_section =
else if(cf)
type = cf->origin_type;
else
- die("BUG: current_config_origin_type called outside config callback");
+ BUG("current_config_origin_type called outside config callback");
switch (type) {
case CONFIG_ORIGIN_BLOB:
case CONFIG_ORIGIN_CMDLINE:
return "command line";
default:
- die("BUG: unknown config origin type");
+ BUG("unknown config origin type");
}
}
else if (cf)
name = cf->name;
else
- die("BUG: current_config_name called outside config callback");
+ BUG("current_config_name called outside config callback");
return name ? name : "";
}
HAVE_GETDELIM = YesPlease
SANE_TEXT_GREP=-a
FREAD_READS_DIRECTORIES = UnfortunatelyYes
+ BASIC_CFLAGS += -DHAVE_SYSINFO
PROCFS_EXECUTABLE_PATH = /proc/self/exe
endif
ifeq ($(uname_S),GNU/kFreeBSD)
struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
struct ref **list, int for_push,
- const struct argv_array *ref_prefixes)
+ const struct argv_array *ref_prefixes,
+ const struct string_list *server_options)
{
int i;
*list = NULL;
if (server_supports_v2("agent", 0))
packet_write_fmt(fd_out, "agent=%s", git_user_agent_sanitized());
+ if (server_options && server_options->nr &&
+ server_supports_v2("server-option", 1))
+ for (i = 0; i < server_options->nr; i++)
+ packet_write_fmt(fd_out, "server-option=%s",
+ server_options->items[i].string);
+
packet_delim(fd_out);
/* When pushing we don't want to request the peeled tags */
if (!for_push)
--- /dev/null
+@@
+expression c;
+@@
+- &c->maybe_tree->object.oid
++ get_commit_tree_oid(c)
+
+@@
+expression c;
+@@
+- c->maybe_tree->object.oid.hash
++ get_commit_tree_oid(c)->hash
+
+// These excluded functions must access c->maybe_tree direcly.
+@@
+identifier f !~ "^(get_commit_tree|get_commit_tree_in_graph|load_tree_for_commit)$";
+expression c;
+@@
+ f(...) {...
+- c->maybe_tree
++ get_commit_tree(c)
+ ...}
+
+@@
+expression c;
+expression s;
+@@
+- get_commit_tree(c) = s
++ c->maybe_tree = s
${__git_dir:+--git-dir="$__git_dir"} "$@" 2>/dev/null
}
+# Removes backslash escaping, single quotes and double quotes from a word,
+# stores the result in the variable $dequoted_word.
+# 1: The word to dequote.
+__git_dequote ()
+{
+ local rest="$1" len ch
+
+ dequoted_word=""
+
+ while test -n "$rest"; do
+ len=${#dequoted_word}
+ dequoted_word="$dequoted_word${rest%%[\\\'\"]*}"
+ rest="${rest:$((${#dequoted_word}-$len))}"
+
+ case "${rest:0:1}" in
+ \\)
+ ch="${rest:1:1}"
+ case "$ch" in
+ $'\n')
+ ;;
+ *)
+ dequoted_word="$dequoted_word$ch"
+ ;;
+ esac
+ rest="${rest:2}"
+ ;;
+ \')
+ rest="${rest:1}"
+ len=${#dequoted_word}
+ dequoted_word="$dequoted_word${rest%%\'*}"
+ rest="${rest:$((${#dequoted_word}-$len+1))}"
+ ;;
+ \")
+ rest="${rest:1}"
+ while test -n "$rest" ; do
+ len=${#dequoted_word}
+ dequoted_word="$dequoted_word${rest%%[\\\"]*}"
+ rest="${rest:$((${#dequoted_word}-$len))}"
+ case "${rest:0:1}" in
+ \\)
+ ch="${rest:1:1}"
+ case "$ch" in
+ \"|\\|\$|\`)
+ dequoted_word="$dequoted_word$ch"
+ ;;
+ $'\n')
+ ;;
+ *)
+ dequoted_word="$dequoted_word\\$ch"
+ ;;
+ esac
+ rest="${rest:2}"
+ ;;
+ \")
+ rest="${rest:1}"
+ break
+ ;;
+ esac
+ done
+ ;;
+ esac
+ done
+}
+
# The following function is based on code from:
#
# bash_completion - programmable completion functions for bash 3.2+
__gitcomp_nl_append "$@"
}
+# Fills the COMPREPLY array with prefiltered paths without any additional
+# processing.
+# Callers must take care of providing only paths that match the current path
+# to be completed and adding any prefix path components, if necessary.
+# 1: List of newline-separated matching paths, complete with all prefix
+# path componens.
+__gitcomp_file_direct ()
+{
+ local IFS=$'\n'
+
+ COMPREPLY=($1)
+
+ # use a hack to enable file mode in bash < 4
+ compopt -o filenames +o nospace 2>/dev/null ||
+ compgen -f /non-existing-dir/ >/dev/null ||
+ true
+}
+
# Generates completion reply with compgen from newline-separated possible
# completion filenames.
# It accepts 1 to 3 arguments:
# use a hack to enable file mode in bash < 4
compopt -o filenames +o nospace 2>/dev/null ||
- compgen -f /non-existing-dir/ > /dev/null
+ compgen -f /non-existing-dir/ >/dev/null ||
+ true
}
# Execute 'git ls-files', unless the --committable option is specified, in
__git_ls_files_helper ()
{
if [ "$2" == "--committable" ]; then
- __git -C "$1" diff-index --name-only --relative HEAD
+ __git -C "$1" -c core.quotePath=false diff-index \
+ --name-only --relative HEAD -- "${3//\\/\\\\}*"
else
# NOTE: $2 is not quoted in order to support multiple options
- __git -C "$1" ls-files --exclude-standard $2
+ __git -C "$1" -c core.quotePath=false ls-files \
+ --exclude-standard $2 -- "${3//\\/\\\\}*"
fi
}
# If provided, only files within the specified directory are listed.
# Sub directories are never recursed. Path must have a trailing
# slash.
+# 3: List only paths matching this path component (optional).
__git_index_files ()
{
- local root="${2-.}" file
+ local root="$2" match="$3"
- __git_ls_files_helper "$root" "$1" |
- cut -f1 -d/ | sort | uniq
+ __git_ls_files_helper "$root" "$1" "$match" |
+ awk -F / -v pfx="${2//\\/\\\\}" '{
+ paths[$1] = 1
+ }
+ END {
+ for (p in paths) {
+ if (substr(p, 1, 1) != "\"") {
+ # No special characters, easy!
+ print pfx p
+ continue
+ }
+
+ # The path is quoted.
+ p = dequote(p)
+ if (p == "")
+ continue
+
+ # Even when a directory name itself does not contain
+ # any special characters, it will still be quoted if
+ # any of its (stripped) trailing path components do.
+ # Because of this we may have seen the same direcory
+ # both quoted and unquoted.
+ if (p in paths)
+ # We have seen the same directory unquoted,
+ # skip it.
+ continue
+ else
+ print pfx p
+ }
+ }
+ function dequote(p, bs_idx, out, esc, esc_idx, dec) {
+ # Skip opening double quote.
+ p = substr(p, 2)
+
+ # Interpret backslash escape sequences.
+ while ((bs_idx = index(p, "\\")) != 0) {
+ out = out substr(p, 1, bs_idx - 1)
+ esc = substr(p, bs_idx + 1, 1)
+ p = substr(p, bs_idx + 2)
+
+ if ((esc_idx = index("abtvfr\"\\", esc)) != 0) {
+ # C-style one-character escape sequence.
+ out = out substr("\a\b\t\v\f\r\"\\",
+ esc_idx, 1)
+ } else if (esc == "n") {
+ # Uh-oh, a newline character.
+ # We cant reliably put a pathname
+ # containing a newline into COMPREPLY,
+ # and the newline would create a mess.
+ # Skip this path.
+ return ""
+ } else {
+ # Must be a \nnn octal value, then.
+ dec = esc * 64 + \
+ substr(p, 1, 1) * 8 + \
+ substr(p, 2, 1)
+ out = out sprintf("%c", dec)
+ p = substr(p, 3)
+ }
+ }
+ # Drop closing double quote, if there is one.
+ # (There isnt any if this is a directory, as it was
+ # already stripped with the trailing path components.)
+ if (substr(p, length(p), 1) == "\"")
+ out = out substr(p, 1, length(p) - 1)
+ else
+ out = out p
+
+ return out
+ }'
+}
+
+# __git_complete_index_file requires 1 argument:
+# 1: the options to pass to ls-file
+#
+# The exception is --committable, which finds the files appropriate commit.
+__git_complete_index_file ()
+{
+ local dequoted_word pfx="" cur_
+
+ __git_dequote "$cur"
+
+ case "$dequoted_word" in
+ ?*/*)
+ pfx="${dequoted_word%/*}/"
+ cur_="${dequoted_word##*/}"
+ ;;
+ *)
+ cur_="$dequoted_word"
+ esac
+
+ __gitcomp_file_direct "$(__git_index_files "$1" "$pfx" "$cur_")"
}
# Lists branches from the local repository.
esac
}
-
-# __git_complete_index_file requires 1 argument:
-# 1: the options to pass to ls-file
-#
-# The exception is --committable, which finds the files appropriate commit.
-__git_complete_index_file ()
-{
- local pfx="" cur_="$cur"
-
- case "$cur_" in
- ?*/*)
- pfx="${cur_%/*}"
- cur_="${cur_##*/}"
- pfx="${pfx}/"
- ;;
- esac
-
- __gitcomp_file "$(__git_index_files "$1" ${pfx:+"$pfx"})" "$pfx" "$cur_"
-}
-
__git_complete_file ()
{
__git_complete_revlist_file
--*)
__gitcomp "
--onto --merge --strategy --interactive
- --preserve-merges --stat --no-stat
+ --rebase-merges --preserve-merges --stat --no-stat
--committer-date-is-author-date --ignore-date
--ignore-whitespace --whitespace=
--autosquash --no-autosquash
return
;;
branch.*.rebase)
- __gitcomp "false true preserve interactive"
+ __gitcomp "false true merges preserve interactive"
return
;;
remote.pushdefault)
__gitcomp "$__git_log_date_formats"
return
;;
- sendemail.aliasesfiletype)
+ sendemail.aliasfiletype)
__gitcomp "mutt mailrc pine elm gnus"
return
;;
__git_complete_command () {
local command="$1"
local completion_func="_git_${command//-/_}"
- if declare -f $completion_func >/dev/null 2>/dev/null; then
+ if ! declare -f $completion_func >/dev/null 2>/dev/null &&
+ declare -f _completion_loader >/dev/null 2>/dev/null
+ then
+ _completion_loader "git-$command"
+ fi
+ if declare -f $completion_func >/dev/null 2>/dev/null
+ then
$completion_func
return 0
- elif __git_support_parseopt_helper "$command"; then
+ elif __git_support_parseopt_helper "$command"
+ then
__git_complete_common "$command"
return 0
else
compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0
}
+ __gitcomp_file_direct ()
+ {
+ emulate -L zsh
+
+ local IFS=$'\n'
+ compset -P '*[=:]'
+ compadd -Q -f -- ${=1} && _ret=0
+ }
+
__gitcomp_file ()
{
emulate -L zsh
compadd -Q -S "${4- }" -p "${2-}" -- ${=1} && _ret=0
}
+__gitcomp_file_direct ()
+{
+ emulate -L zsh
+
+ local IFS=$'\n'
+ compset -P '*[=:]'
+ compadd -Q -f -- ${=1} && _ret=0
+}
+
__gitcomp_file ()
{
emulate -L zsh
+++ /dev/null
-#!/bin/sh
-
-# You should execute this script in the repository where you
-# want to convert grafts to replace refs.
-
-GRAFTS_FILE="${GIT_DIR:-.git}/info/grafts"
-
-. $(git --exec-path)/git-sh-setup
-
-test -f "$GRAFTS_FILE" || die "Could not find graft file: '$GRAFTS_FILE'"
-
-grep '^[^# ]' "$GRAFTS_FILE" |
-while read definition
-do
- if test -n "$definition"
- then
- echo "Converting: $definition"
- git replace --graft $definition ||
- die "Conversion failed for: $definition"
- fi
-done
-
-mv "$GRAFTS_FILE" "$GRAFTS_FILE.bak" ||
- die "Could not rename '$GRAFTS_FILE' to '$GRAFTS_FILE.bak'"
-
-echo "Success!"
-echo "All the grafts in '$GRAFTS_FILE' have been converted to replace refs!"
-echo "The grafts file '$GRAFTS_FILE' has been renamed: '$GRAFTS_FILE.bak'"
test:
- ./test.pl
+ ./t-git-credential-netrc.sh
testverbose:
- ./test.pl -d -v
+ ./t-git-credential-netrc.sh -d -v
use strict;
use warnings;
+use autodie;
use Getopt::Long;
use File::Basename;
+use Git;
-my $VERSION = "0.1";
+my $VERSION = "0.2";
my %options = (
help => 0,
"insecure|k",
"verbose|v",
"file|f=s@",
+ 'gpg|g:s',
);
if ($options{help}) {
print <<EOHIPPUS;
-$0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] [-v] [-k] get
+$0 [(-f <authfile>)...] [-g <program>] [-d] [-v] [-k] get
Version $VERSION by tzz\@lifelogs.com. License: BSD.
Options:
- -f|--file AUTHFILE : specify netrc-style files. Files with the .gpg extension
- will be decrypted by GPG before parsing. Multiple -f
- arguments are OK. They are processed in order, and the
- first matching entry found is returned via the credential
- helper protocol (see below).
+ -f|--file <authfile>: specify netrc-style files. Files with the .gpg
+ extension will be decrypted by GPG before parsing.
+ Multiple -f arguments are OK. They are processed in
+ order, and the first matching entry found is returned
+ via the credential helper protocol (see below).
- When no -f option is given, .authinfo.gpg, .netrc.gpg,
- .authinfo, and .netrc files in your home directory are used
- in this order.
+ When no -f option is given, .authinfo.gpg, .netrc.gpg,
+ .authinfo, and .netrc files in your home directory are
+ used in this order.
- -k|--insecure : ignore bad file ownership or permissions
+ -g|--gpg <program> : specify the program for GPG. By default, this is the
+ value of gpg.program in the git repository or global
+ option or gpg.
- -d|--debug : turn on debugging (developer info)
+ -k|--insecure : ignore bad file ownership or permissions
- -v|--verbose : be more verbose (show files and information found)
+ -d|--debug : turn on debugging (developer info)
+
+ -v|--verbose : be more verbose (show files and information found)
To enable this credential helper:
git config credential.helper '$shortname -f AUTHFILE -v'
-Only "get" mode is supported by this credential helper. It opens every AUTHFILE
-and looks for the first entry that matches the requested search criteria:
+Only "get" mode is supported by this credential helper. It opens every
+<authfile> and looks for the first entry that matches the requested search
+criteria:
'port|protocol':
The protocol that will be used (e.g., https). (protocol=X)
protocol=https
username=tzz
-this credential helper will look for the first entry in every AUTHFILE that
+this credential helper will look for the first entry in every <authfile> that
matches
machine github.com port https login tzz
back to "protocol". Any redundant entry tokens (part of the original query) are
skipped.
-Again, note that only the first matching entry from all the AUTHFILEs, processed
-in the sequence given on the command line, is used.
+Again, note that only the first matching entry from all the <authfile>s,
+processed in the sequence given on the command line, is used.
Netrc/authinfo tokens can be quoted as 'STRING' or "STRING".
my $mode = shift @ARGV;
# Credentials must get a parameter, so die if it's missing.
-die "Syntax: $0 [-f AUTHFILE1] [-f AUTHFILEN] [-d] get" unless defined $mode;
+die "Syntax: $0 [(-f <authfile>)...] [-d] get" unless defined $mode;
# Only support 'get' mode; with any other unsupported ones we just exit.
exit 0 unless $mode eq 'get';
$files = $options{file} = [ map { glob $_ } @candidates ];
}
+load_config(\%options);
+
my $query = read_credential_data_from_stdin();
FILE:
my $io;
if ($gpgmode) {
- my @cmd = (qw(gpg --decrypt), $file);
+ my @cmd = ($options{'gpg'}, qw(--decrypt), $file);
log_verbose("Using GPG to open $file: [@cmd]");
open $io, "-|", @cmd;
} else {
printf "%s=%s\n", $git_token, $entry->{$git_token};
}
}
+sub load_config {
+ # load settings from git config
+ my $options = shift;
+ # set from command argument, gpg.program option, or default to gpg
+ $options->{'gpg'} //= Git->repository()->config('gpg.program')
+ // 'gpg';
+ log_verbose("using $options{'gpg'} for GPG operations");
+}
sub log_verbose {
return unless $options{verbose};
printf STDERR @_;
--- /dev/null
+#!/bin/sh
+(
+ cd ../../../t
+ test_description='git-credential-netrc'
+ . ./test-lib.sh
+
+ if ! test_have_prereq PERL; then
+ skip_all='skipping perl interface tests, perl not available'
+ test_done
+ fi
+
+ perl -MTest::More -e 0 2>/dev/null || {
+ skip_all="Perl Test::More unavailable, skipping test"
+ test_done
+ }
+
+ # set up test repository
+
+ test_expect_success \
+ 'set up test repository' \
+ 'git config --add gpg.program test.git-config-gpg'
+
+ # The external test will outputs its own plan
+ test_external_has_tap=1
+
+ test_external \
+ 'git-credential-netrc' \
+ perl "$TEST_DIRECTORY"/../contrib/credential/netrc/test.pl
+
+ test_done
+)
--- /dev/null
+#!/bin/sh
+echo machine command-option-gpg login username password password
--- /dev/null
+#!/bin/sh
+echo machine git-config-gpg login username password password
#!/usr/bin/perl
+use lib (split(/:/, $ENV{GITPERLLIB}));
use warnings;
use strict;
-use Test;
+use Test::More qw(no_plan);
+use File::Basename;
+use File::Spec::Functions qw(:DEFAULT rel2abs);
use IPC::Open2;
-BEGIN { plan tests => 15 }
+BEGIN {
+ # t-git-credential-netrc.sh kicks off our testing, so we have to go
+ # from there.
+ Test::More->builder->current_test(1);
+ Test::More->builder->no_ending(1);
+}
my @global_credential_args = @ARGV;
-my $netrc = './test.netrc';
-print "# Testing insecure file, nothing should be found\n";
+my $scriptDir = dirname rel2abs $0;
+my ($netrc, $netrcGpg, $gcNetrc) = map { catfile $scriptDir, $_; }
+ qw(test.netrc
+ test.netrc.gpg
+ git-credential-netrc);
+local $ENV{PATH} = join ':'
+ , $scriptDir
+ , $ENV{PATH}
+ ? $ENV{PATH}
+ : ();
+
+diag "Testing insecure file, nothing should be found\n";
chmod 0644, $netrc;
my $cred = run_credential(['-f', $netrc, 'get'],
{ host => 'github.com' });
-ok(scalar keys %$cred, 0, "Got 0 keys from insecure file");
+ok(scalar keys %$cred == 0, "Got 0 keys from insecure file");
-print "# Testing missing file, nothing should be found\n";
+diag "Testing missing file, nothing should be found\n";
chmod 0644, $netrc;
$cred = run_credential(['-f', '///nosuchfile///', 'get'],
{ host => 'github.com' });
-ok(scalar keys %$cred, 0, "Got 0 keys from missing file");
+ok(scalar keys %$cred == 0, "Got 0 keys from missing file");
chmod 0600, $netrc;
-print "# Testing with invalid data\n";
+diag "Testing with invalid data\n";
$cred = run_credential(['-f', $netrc, 'get'],
"bad data");
-ok(scalar keys %$cred, 4, "Got first found keys with bad data");
+ok(scalar keys %$cred == 4, "Got first found keys with bad data");
-print "# Testing netrc file for a missing corovamilkbar entry\n";
+diag "Testing netrc file for a missing corovamilkbar entry\n";
$cred = run_credential(['-f', $netrc, 'get'],
{ host => 'corovamilkbar' });
-ok(scalar keys %$cred, 0, "Got no corovamilkbar keys");
+ok(scalar keys %$cred == 0, "Got no corovamilkbar keys");
-print "# Testing netrc file for a github.com entry\n";
+diag "Testing netrc file for a github.com entry\n";
$cred = run_credential(['-f', $netrc, 'get'],
{ host => 'github.com' });
-ok(scalar keys %$cred, 2, "Got 2 Github keys");
+ok(scalar keys %$cred == 2, "Got 2 Github keys");
-ok($cred->{password}, 'carolknows', "Got correct Github password");
-ok($cred->{username}, 'carol', "Got correct Github username");
+is($cred->{password}, 'carolknows', "Got correct Github password");
+is($cred->{username}, 'carol', "Got correct Github username");
-print "# Testing netrc file for a username-specific entry\n";
+diag "Testing netrc file for a username-specific entry\n";
$cred = run_credential(['-f', $netrc, 'get'],
{ host => 'imap', username => 'bob' });
-ok(scalar keys %$cred, 2, "Got 2 username-specific keys");
+ok(scalar keys %$cred == 2, "Got 2 username-specific keys");
-ok($cred->{password}, 'bobwillknow', "Got correct user-specific password");
-ok($cred->{protocol}, 'imaps', "Got correct user-specific protocol");
+is($cred->{password}, 'bobwillknow', "Got correct user-specific password");
+is($cred->{protocol}, 'imaps', "Got correct user-specific protocol");
-print "# Testing netrc file for a host:port-specific entry\n";
+diag "Testing netrc file for a host:port-specific entry\n";
$cred = run_credential(['-f', $netrc, 'get'],
{ host => 'imap2:1099' });
-ok(scalar keys %$cred, 2, "Got 2 host:port-specific keys");
+ok(scalar keys %$cred == 2, "Got 2 host:port-specific keys");
-ok($cred->{password}, 'tzzknow', "Got correct host:port-specific password");
-ok($cred->{username}, 'tzz', "Got correct host:port-specific username");
+is($cred->{password}, 'tzzknow', "Got correct host:port-specific password");
+is($cred->{username}, 'tzz', "Got correct host:port-specific username");
-print "# Testing netrc file that 'host:port kills host' entry\n";
+diag "Testing netrc file that 'host:port kills host' entry\n";
$cred = run_credential(['-f', $netrc, 'get'],
{ host => 'imap2' });
-ok(scalar keys %$cred, 2, "Got 2 'host:port kills host' keys");
+ok(scalar keys %$cred == 2, "Got 2 'host:port kills host' keys");
+
+is($cred->{password}, 'bobwillknow', "Got correct 'host:port kills host' password");
+is($cred->{username}, 'bob', "Got correct 'host:port kills host' username");
+
+diag 'Testing netrc file decryption by git config gpg.program setting\n';
+$cred = run_credential( ['-f', $netrcGpg, 'get']
+ , { host => 'git-config-gpg' }
+ );
+
+ok(scalar keys %$cred == 2, 'Got keys decrypted by git config option');
+
+diag 'Testing netrc file decryption by gpg option\n';
+$cred = run_credential( ['-f', $netrcGpg, '-g', 'test.command-option-gpg', 'get']
+ , { host => 'command-option-gpg' }
+ );
-ok($cred->{password}, 'bobwillknow', "Got correct 'host:port kills host' password");
-ok($cred->{username}, 'bob', "Got correct 'host:port kills host' username");
+ok(scalar keys %$cred == 2, 'Got keys decrypted by command option');
sub run_credential
{
my $args = shift @_;
my $data = shift @_;
my $pid = open2(my $chld_out, my $chld_in,
- './git-credential-netrc', @global_credential_args,
+ $gcNetrc, @global_credential_args,
@$args);
die "Couldn't open pipe to netrc credential helper: $!" unless $pid;
{
static struct date_mode mode;
if (type == DATE_STRFTIME)
- die("BUG: cannot create anonymous strftime date_mode struct");
+ BUG("cannot create anonymous strftime date_mode struct");
mode.type = type;
mode.local = 0;
return &mode;
return 0;
}
-static int git_config_rename(const char *var, const char *value)
+int git_config_rename(const char *var, const char *value)
{
if (!value)
return DIFF_DETECT_RENAME;
fputs(o->stat_sep, o->file);
break;
default:
- die("BUG: unknown diff symbol");
+ BUG("unknown diff symbol");
}
strbuf_release(&sb);
}
for (i = 0; i < ARRAY_SIZE(diff_temp); i++)
if (!diff_temp[i].name)
return diff_temp + i;
- die("BUG: diff is failing to clean up its tempfiles");
+ BUG("diff is failing to clean up its tempfiles");
}
static void remove_tempfile(void)
* objects however would tend to be slower as they need
* to be individually opened and inflated.
*/
- if (!FAST_WORKING_DIRECTORY && !want_file && has_sha1_pack(oid->hash))
+ if (!FAST_WORKING_DIRECTORY && !want_file && has_object_pack(oid))
return 0;
/*
else {
enum object_type type;
if (size_only || (flags & CHECK_BINARY)) {
- type = oid_object_info(&s->oid, &s->size);
+ type = oid_object_info(the_repository, &s->oid,
+ &s->size);
if (type < 0)
die("unable to read %s",
oid_to_hex(&s->oid));
if (abbrev < 0)
abbrev = FALLBACK_DEFAULT_ABBREV;
if (abbrev > GIT_SHA1_HEXSZ)
- die("BUG: oid abbreviation out of range: %d", abbrev);
+ BUG("oid abbreviation out of range: %d", abbrev);
if (abbrev)
hex[abbrev] = '\0';
return hex;
*must_show_header = 0;
}
if (one && two && oidcmp(&one->oid, &two->oid)) {
- int abbrev = o->flags.full_index ? 40 : DEFAULT_ABBREV;
+ const unsigned hexsz = the_hash_algo->hexsz;
+ int abbrev = o->flags.full_index ? hexsz : DEFAULT_ABBREV;
if (o->flags.binary) {
mmfile_t mf;
if ((!fill_mmfile(&mf, one) && diff_filespec_is_binary(one)) ||
(!fill_mmfile(&mf, two) && diff_filespec_is_binary(two)))
- abbrev = 40;
+ abbrev = hexsz;
}
strbuf_addf(msg, "%s%sindex %s..%s", line_prefix, set,
diff_abbrev_oid(&one->oid, abbrev),
DIFF_FORMAT_NAME_STATUS |
DIFF_FORMAT_CHECKDIFF |
DIFF_FORMAT_NO_OUTPUT;
+ /*
+ * This must be signed because we're comparing against a potentially
+ * negative value.
+ */
+ const int hexsz = the_hash_algo->hexsz;
if (options->set_default)
options->set_default(options);
*/
read_cache();
}
- if (40 < options->abbrev)
- options->abbrev = 40; /* full */
+ if (hexsz < options->abbrev)
+ options->abbrev = hexsz; /* full */
/*
* It does not make sense to show the first hit we happened
int argcount = 1;
if (!skip_prefix(arg, "--stat", &arg))
- die("BUG: stat option does not begin with --stat: %s", arg);
+ BUG("stat option does not begin with --stat: %s", arg);
end = (char *)arg;
switch (*arg) {
options->abbrev = strtoul(arg, NULL, 10);
if (options->abbrev < MINIMUM_ABBREV)
options->abbrev = MINIMUM_ABBREV;
- else if (40 < options->abbrev)
- options->abbrev = 40;
+ else if (the_hash_algo->hexsz < options->abbrev)
+ options->abbrev = the_hash_algo->hexsz;
}
else if ((argcount = parse_long_opt("src-prefix", av, &optarg))) {
options->a_prefix = optarg;
struct diff_queue_struct *q = &diff_queued_diff;
if (WSEH_NEW & WS_RULE_MASK)
- die("BUG: WS rules bit mask overlaps with diff symbol flags");
+ BUG("WS rules bit mask overlaps with diff symbol flags");
if (o->color_moved)
o->emitted_symbols = &esm;
}
if (!driver->textconv)
- die("BUG: fill_textconv called with non-textconv driver");
+ BUG("fill_textconv called with non-textconv driver");
if (driver->textconv_cache && df->oid_valid) {
*outbuf = notes_cache_get(driver->textconv_cache,
extern void diff_setup(struct diff_options *);
extern int diff_opt_parse(struct diff_options *, const char **, int, const char *);
extern void diff_setup_done(struct diff_options *);
+extern int git_config_rename(const char *var, const char *value);
#define DIFF_DETECT_RENAME 1
#define DIFF_DETECT_COPY 2
struct dir_iterator *dir_iterator = &iter->base;
if (!path || !*path)
- die("BUG: empty path passed to dir_iterator_begin()");
+ BUG("empty path passed to dir_iterator_begin()");
strbuf_init(&iter->base.path, PATH_MAX);
strbuf_addstr(&iter->base.path, path);
if (size == 0) {
if (oid_stat) {
fill_stat_data(&oid_stat->stat, &st);
- oidcpy(&oid_stat->oid, &empty_blob_oid);
+ oidcpy(&oid_stat->oid, the_hash_algo->empty_blob);
oid_stat->valid = 1;
}
close(fd);
(!untracked || !untracked->valid ||
/*
* .. and .gitignore does not exist before
- * (i.e. null exclude_sha1). Then we can skip
+ * (i.e. null exclude_oid). Then we can skip
* loading .gitignore, which would result in
* ENOENT anyway.
*/
- !is_null_sha1(untracked->exclude_sha1))) {
+ !is_null_oid(&untracked->exclude_oid))) {
/*
* dir->basebuf gets reused by the traversal, but we
* need fname to remain unchanged to ensure the src
* order, though, if you do that.
*/
if (untracked &&
- hashcmp(oid_stat.oid.hash, untracked->exclude_sha1)) {
+ oidcmp(&oid_stat.oid, &untracked->exclude_oid)) {
invalidate_gitignore(dir->untracked, untracked);
- hashcpy(untracked->exclude_sha1, oid_stat.oid.hash);
+ oidcpy(&untracked->exclude_oid, &oid_stat.oid);
}
dir->exclude_stack = stk;
current = stk->baselen;
stat_data_to_disk(&stat_data, &untracked->stat_data);
strbuf_add(&wd->sb_stat, &stat_data, sizeof(stat_data));
}
- if (!is_null_sha1(untracked->exclude_sha1)) {
+ if (!is_null_oid(&untracked->exclude_oid)) {
ewah_set(wd->sha1_valid, i);
- strbuf_add(&wd->sb_sha1, untracked->exclude_sha1, 20);
+ strbuf_add(&wd->sb_sha1, untracked->exclude_oid.hash,
+ the_hash_algo->rawsz);
}
intlen = encode_varint(untracked->untracked_nr, intbuf);
ud->valid = 1;
}
-static void read_sha1(size_t pos, void *cb)
+static void read_oid(size_t pos, void *cb)
{
struct read_data *rd = cb;
struct untracked_cache_dir *ud = rd->ucd[pos];
- if (rd->data + 20 > rd->end) {
+ if (rd->data + the_hash_algo->rawsz > rd->end) {
rd->data = rd->end + 1;
return;
}
- hashcpy(ud->exclude_sha1, rd->data);
- rd->data += 20;
+ hashcpy(ud->exclude_oid.hash, rd->data);
+ rd->data += the_hash_algo->rawsz;
}
static void load_oid_stat(struct oid_stat *oid_stat, const unsigned char *data,
ewah_each_bit(rd.check_only, set_check_only, &rd);
rd.data = next + len;
ewah_each_bit(rd.valid, read_stat, &rd);
- ewah_each_bit(rd.sha1_valid, read_sha1, &rd);
+ ewah_each_bit(rd.sha1_valid, read_oid, &rd);
next = rd.data;
done:
{
if (!istate->untracked || !istate->untracked->root)
return;
- if (!safe_path && !verify_path(path))
+ if (!safe_path && !verify_path(path, 0))
return;
invalidate_one_component(istate->untracked, istate->untracked->root,
path, strlen(path));
/* See Documentation/technical/api-directory-listing.txt */
+#include "cache.h"
#include "strbuf.h"
struct dir_entry {
/* all data except 'dirs' in this struct are good */
unsigned int valid : 1;
unsigned int recurse : 1;
- /* null SHA-1 means this directory does not have .gitignore */
- unsigned char exclude_sha1[20];
+ /* null object ID means this directory does not have .gitignore */
+ struct object_id exclude_oid;
char name[FLEX_ARRAY];
};
*/
p->pack_size = pack_size + the_hash_algo->rawsz;
}
- return unpack_entry(p, oe->idx.offset, &type, sizep);
+ return unpack_entry(the_repository, p, oe->idx.offset, &type, sizep);
}
static const char *get_mode(const char *str, uint16_t *modep)
static void dump_marks(void)
{
- static struct lock_file mark_lock;
+ struct lock_file mark_lock = LOCK_INIT;
FILE *f;
if (!export_marks_file || (import_marks_file && !import_marks_file_done))
die("corrupt mark line: %s", line);
e = find_object(&oid);
if (!e) {
- enum object_type type = oid_object_info(&oid, NULL);
+ enum object_type type = oid_object_info(the_repository,
+ &oid, NULL);
if (type < 0)
die("object not found: %s", oid_to_hex(&oid));
e = insert_object(&oid);
enum object_type expected = S_ISDIR(mode) ?
OBJ_TREE: OBJ_BLOB;
enum object_type type = oe ? oe->type :
- oid_object_info(&oid, NULL);
+ oid_object_info(the_repository, &oid,
+ NULL);
if (type < 0)
die("%s not found: %s",
S_ISDIR(mode) ? "Tree" : "Blob",
die("Not a blob (actually a %s): %s",
type_name(oe->type), command_buf.buf);
} else if (!is_null_oid(&oid)) {
- enum object_type type = oid_object_info(&oid, NULL);
+ enum object_type type = oid_object_info(the_repository, &oid,
+ NULL);
if (type < 0)
die("Blob not found: %s", command_buf.buf);
if (type != OBJ_BLOB)
} else if (!get_oid(from, &oid)) {
struct object_entry *oe = find_object(&oid);
if (!oe) {
- type = oid_object_info(&oid, NULL);
+ type = oid_object_info(the_repository, &oid, NULL);
if (type < 0)
die("Not a valid object: %s", from);
} else
unsigned long size;
char *buf = NULL;
if (!oe) {
- enum object_type type = oid_object_info(oid, NULL);
+ enum object_type type = oid_object_info(the_repository, oid,
+ NULL);
if (type < 0)
die("object not found: %s", oid_to_hex(oid));
/* cache it! */
packet_buf_write(&req_buf, "command=fetch");
if (server_supports_v2("agent", 0))
packet_buf_write(&req_buf, "agent=%s", git_user_agent_sanitized());
+ if (args->server_options && args->server_options->nr &&
+ server_supports_v2("server-option", 1)) {
+ int i;
+ for (i = 0; i < args->server_options->nr; i++)
+ packet_write_fmt(fd_out, "server-option=%s",
+ args->server_options->items[i].string);
+ }
packet_buf_delim(&req_buf);
if (args->use_thin_pack)
else if (is_repository_shallow() || args->deepen)
die(_("Server does not support shallow requests"));
+ /* Add filter */
+ if (server_supports_feature("fetch", "filter", 0) &&
+ args->filter_options.choice) {
+ print_verbose(args, _("Server supports filter"));
+ packet_buf_write(&req_buf, "filter %s",
+ args->filter_options.filter_spec);
+ } else if (args->filter_options.choice) {
+ warning("filtering not recognized by server, ignoring");
+ }
+
/* add wants */
add_wants(wants, &req_buf);
- /* Add all of the common commits we've found in previous rounds */
- add_common(&req_buf, common);
+ if (args->no_dependents) {
+ packet_buf_write(&req_buf, "done");
+ ret = 1;
+ } else {
+ /* Add all of the common commits we've found in previous rounds */
+ add_common(&req_buf, common);
- /* Add initial haves */
- ret = add_haves(&req_buf, haves_to_send, in_vain);
+ /* Add initial haves */
+ ret = add_haves(&req_buf, haves_to_send, in_vain);
+ }
/* Send request */
packet_buf_flush(&req_buf);
const char *deepen_since;
const struct string_list *deepen_not;
struct list_objects_filter_options filter_options;
+ const struct string_list *server_options;
unsigned deepen_relative:1;
unsigned quiet:1;
unsigned keep_pack:1;
#include "utf8.h"
#include "sha1-array.h"
#include "decorate.h"
+#include "oidset.h"
+#include "packfile.h"
+#include "submodule-config.h"
+#include "config.h"
+
+static struct oidset gitmodules_found = OIDSET_INIT;
+static struct oidset gitmodules_done = OIDSET_INIT;
#define FSCK_FATAL -1
#define FSCK_INFO -2
FUNC(MISSING_TAG_ENTRY, ERROR) \
FUNC(MISSING_TAG_OBJECT, ERROR) \
FUNC(MISSING_TREE, ERROR) \
+ FUNC(MISSING_TREE_OBJECT, ERROR) \
FUNC(MISSING_TYPE, ERROR) \
FUNC(MISSING_TYPE_ENTRY, ERROR) \
FUNC(MULTIPLE_AUTHORS, ERROR) \
FUNC(TREE_NOT_SORTED, ERROR) \
FUNC(UNKNOWN_TYPE, ERROR) \
FUNC(ZERO_PADDED_DATE, ERROR) \
+ FUNC(GITMODULES_MISSING, ERROR) \
+ FUNC(GITMODULES_BLOB, ERROR) \
+ FUNC(GITMODULES_PARSE, ERROR) \
+ FUNC(GITMODULES_NAME, ERROR) \
+ FUNC(GITMODULES_SYMLINK, ERROR) \
/* warnings */ \
FUNC(BAD_FILEMODE, WARN) \
FUNC(EMPTY_NAME, WARN) \
name = get_object_name(options, &commit->object);
if (name)
- put_object_name(options, &commit->tree->object, "%s:", name);
+ put_object_name(options, &get_commit_tree(commit)->object,
+ "%s:", name);
- result = options->walk((struct object *)commit->tree, OBJ_TREE, data, options);
+ result = options->walk((struct object *)get_commit_tree(commit),
+ OBJ_TREE, data, options);
if (result < 0)
return result;
res = result;
has_empty_name |= !*name;
has_dot |= !strcmp(name, ".");
has_dotdot |= !strcmp(name, "..");
- has_dotgit |= (!strcmp(name, ".git") ||
- is_hfs_dotgit(name) ||
- is_ntfs_dotgit(name));
+ has_dotgit |= is_hfs_dotgit(name) || is_ntfs_dotgit(name);
has_zero_pad |= *(char *)desc.buffer == '0';
+
+ if (is_hfs_dotgitmodules(name) || is_ntfs_dotgitmodules(name)) {
+ if (!S_ISLNK(mode))
+ oidset_insert(&gitmodules_found, oid);
+ else
+ retval += report(options, &item->object,
+ FSCK_MSG_GITMODULES_SYMLINK,
+ ".gitmodules is a symbolic link");
+ }
+
if (update_tree_entry_gently(&desc)) {
retval += report(options, &item->object, FSCK_MSG_BAD_TREE, "cannot be parsed as a tree");
break;
static int fsck_commit_buffer(struct commit *commit, const char *buffer,
unsigned long size, struct fsck_options *options)
{
- unsigned char tree_sha1[20], sha1[20];
+ struct object_id tree_oid, oid;
struct commit_graft *graft;
unsigned parent_count, parent_line_count = 0, author_count;
int err;
const char *buffer_begin = buffer;
+ const char *p;
if (verify_headers(buffer, size, &commit->object, options))
return -1;
if (!skip_prefix(buffer, "tree ", &buffer))
return report(options, &commit->object, FSCK_MSG_MISSING_TREE, "invalid format - expected 'tree' line");
- if (get_sha1_hex(buffer, tree_sha1) || buffer[40] != '\n') {
+ if (parse_oid_hex(buffer, &tree_oid, &p) || *p != '\n') {
err = report(options, &commit->object, FSCK_MSG_BAD_TREE_SHA1, "invalid 'tree' line format - bad sha1");
if (err)
return err;
}
- buffer += 41;
+ buffer = p + 1;
while (skip_prefix(buffer, "parent ", &buffer)) {
- if (get_sha1_hex(buffer, sha1) || buffer[40] != '\n') {
+ if (parse_oid_hex(buffer, &oid, &p) || *p != '\n') {
err = report(options, &commit->object, FSCK_MSG_BAD_PARENT_SHA1, "invalid 'parent' line format - bad sha1");
if (err)
return err;
}
- buffer += 41;
+ buffer = p + 1;
parent_line_count++;
}
graft = lookup_commit_graft(&commit->object.oid);
err = fsck_ident(&buffer, &commit->object, options);
if (err)
return err;
- if (!commit->tree) {
- err = report(options, &commit->object, FSCK_MSG_BAD_TREE, "could not load commit's tree %s", sha1_to_hex(tree_sha1));
+ if (!get_commit_tree(commit)) {
+ err = report(options, &commit->object, FSCK_MSG_BAD_TREE, "could not load commit's tree %s", oid_to_hex(&tree_oid));
if (err)
return err;
}
static int fsck_tag_buffer(struct tag *tag, const char *data,
unsigned long size, struct fsck_options *options)
{
- unsigned char sha1[20];
+ struct object_id oid;
int ret = 0;
const char *buffer;
char *to_free = NULL, *eol;
struct strbuf sb = STRBUF_INIT;
+ const char *p;
if (data)
buffer = data;
ret = report(options, &tag->object, FSCK_MSG_MISSING_OBJECT, "invalid format - expected 'object' line");
goto done;
}
- if (get_sha1_hex(buffer, sha1) || buffer[40] != '\n') {
+ if (parse_oid_hex(buffer, &oid, &p) || *p != '\n') {
ret = report(options, &tag->object, FSCK_MSG_BAD_OBJECT_SHA1, "invalid 'object' line format - bad sha1");
if (ret)
goto done;
}
- buffer += 41;
+ buffer = p + 1;
if (!skip_prefix(buffer, "type ", &buffer)) {
ret = report(options, &tag->object, FSCK_MSG_MISSING_TYPE_ENTRY, "invalid format - expected 'type' line");
return fsck_tag_buffer(tag, data, size, options);
}
+struct fsck_gitmodules_data {
+ struct object *obj;
+ struct fsck_options *options;
+ int ret;
+};
+
+static int fsck_gitmodules_fn(const char *var, const char *value, void *vdata)
+{
+ struct fsck_gitmodules_data *data = vdata;
+ const char *subsection, *key;
+ int subsection_len;
+ char *name;
+
+ if (parse_config_key(var, "submodule", &subsection, &subsection_len, &key) < 0 ||
+ !subsection)
+ return 0;
+
+ name = xmemdupz(subsection, subsection_len);
+ if (check_submodule_name(name) < 0)
+ data->ret |= report(data->options, data->obj,
+ FSCK_MSG_GITMODULES_NAME,
+ "disallowed submodule name: %s",
+ name);
+ free(name);
+
+ return 0;
+}
+
+static int fsck_blob(struct blob *blob, const char *buf,
+ unsigned long size, struct fsck_options *options)
+{
+ struct fsck_gitmodules_data data;
+
+ if (!oidset_contains(&gitmodules_found, &blob->object.oid))
+ return 0;
+ oidset_insert(&gitmodules_done, &blob->object.oid);
+
+ if (!buf) {
+ /*
+ * A missing buffer here is a sign that the caller found the
+ * blob too gigantic to load into memory. Let's just consider
+ * that an error.
+ */
+ return report(options, &blob->object,
+ FSCK_MSG_GITMODULES_PARSE,
+ ".gitmodules too large to parse");
+ }
+
+ data.obj = &blob->object;
+ data.options = options;
+ data.ret = 0;
+ if (git_config_from_mem(fsck_gitmodules_fn, CONFIG_ORIGIN_BLOB,
+ ".gitmodules", buf, size, &data))
+ data.ret |= report(options, &blob->object,
+ FSCK_MSG_GITMODULES_PARSE,
+ "could not parse gitmodules blob");
+
+ return data.ret;
+}
+
int fsck_object(struct object *obj, void *data, unsigned long size,
struct fsck_options *options)
{
return report(options, obj, FSCK_MSG_BAD_OBJECT_SHA1, "no valid object to fsck");
if (obj->type == OBJ_BLOB)
- return 0;
+ return fsck_blob((struct blob *)obj, data, size, options);
if (obj->type == OBJ_TREE)
return fsck_tree((struct tree *) obj, options);
if (obj->type == OBJ_COMMIT)
error("object %s: %s", describe_object(o, obj), message);
return 1;
}
+
+int fsck_finish(struct fsck_options *options)
+{
+ int ret = 0;
+ struct oidset_iter iter;
+ const struct object_id *oid;
+
+ oidset_iter_init(&gitmodules_found, &iter);
+ while ((oid = oidset_iter_next(&iter))) {
+ struct blob *blob;
+ enum object_type type;
+ unsigned long size;
+ char *buf;
+
+ if (oidset_contains(&gitmodules_done, oid))
+ continue;
+
+ blob = lookup_blob(oid);
+ if (!blob) {
+ ret |= report(options, &blob->object,
+ FSCK_MSG_GITMODULES_BLOB,
+ "non-blob found at .gitmodules");
+ continue;
+ }
+
+ buf = read_object_file(oid, &type, &size);
+ if (!buf) {
+ if (is_promisor_object(&blob->object.oid))
+ continue;
+ ret |= report(options, &blob->object,
+ FSCK_MSG_GITMODULES_MISSING,
+ "unable to read .gitmodules blob");
+ continue;
+ }
+
+ if (type == OBJ_BLOB)
+ ret |= fsck_blob(blob, buf, size, options);
+ else
+ ret |= report(options, &blob->object,
+ FSCK_MSG_GITMODULES_BLOB,
+ "non-blob found at .gitmodules");
+ free(buf);
+ }
+
+
+ oidset_clear(&gitmodules_found);
+ oidset_clear(&gitmodules_done);
+ return ret;
+}
int fsck_object(struct object *obj, void *data, unsigned long size,
struct fsck_options *options);
+/*
+ * Some fsck checks are context-dependent, and may end up queued; run this
+ * after completing all fsck_object() calls in order to resolve any remaining
+ * checks.
+ */
+int fsck_finish(struct fsck_options *options);
+
#endif
}
}
-sub get_empty_tree {
- return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
+{
+ my $empty_tree;
+ sub get_empty_tree {
+ return $empty_tree if defined $empty_tree;
+
+ $empty_tree = run_cmd_pipe(qw(git hash-object -t tree /dev/null));
+ chomp $empty_tree;
+ return $empty_tree;
+ }
}
sub get_diff_reference {
#include <openssl/err.h>
#endif
+#ifdef HAVE_SYSINFO
+# include <sys/sysinfo.h>
+#endif
+
/* On most systems <netdb.h> would have given us this, but
* not on some systems (e.g. z/OS).
*/
return (x & 0x20) == 0;
}
+/*
+ * Like skip_prefix, but compare case-insensitively. Note that the comparison
+ * is done via tolower(), so it is strictly ASCII (no multi-byte characters or
+ * locale-specific conversions).
+ */
+static inline int skip_iprefix(const char *str, const char *prefix,
+ const char **out)
+{
+ do {
+ if (!*prefix) {
+ *out = str;
+ return 1;
+ }
+ } while (tolower(*str++) == tolower(*prefix++));
+ return 0;
+}
+
static inline int strtoul_ui(char const *s, int base, unsigned int *result)
{
unsigned long ul;
#define QSORT_S(base, n, compar, ctx) do { \
if (qsort_s((base), (n), sizeof(*(base)), compar, ctx)) \
- die("BUG: qsort_s() failed"); \
+ BUG("qsort_s() failed"); \
} while (0)
#ifndef REG_STARTEND
#define HAVE_VARIADIC_MACROS 1
#endif
+/* usage.c: only to be used for testing BUG() implementation (see test-tool) */
+extern int BUG_exit_code;
+
#ifdef HAVE_VARIADIC_MACROS
__attribute__((format (printf, 3, 4))) NORETURN
void BUG_fl(const char *file, int line, const char *fmt, ...);
# The following functions will also be available in the commit filter:
functions=$(cat << \EOF
+EMPTY_TREE=$(git hash-object -t tree /dev/null)
+
warn () {
echo "$*" >&2
}
{
if test $# = 3 && test "$1" = $(git rev-parse "$3^{tree}"); then
map "$3"
- elif test $# = 1 && test "$1" = 4b825dc642cb6eb9a060e54bf8d69288fbee4904; then
+ elif test $# = 1 && test "$1" = $EMPTY_TREE; then
:
else
git commit-tree "$@"
case "$1" in
'')
echo "Added $4 in both, but differently."
- orig=$(git unpack-file e69de29bb2d1d6434b8b29ae775ad8c2e48c5391)
+ orig=$(git unpack-file $(git hash-object /dev/null))
;;
*)
echo "Auto-merging $4"
commits = []
if self.master:
- commitish = self.master
+ committish = self.master
else:
- commitish = 'HEAD'
+ committish = 'HEAD'
- for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, commitish)]):
+ for line in read_pipe_lines(["git", "rev-list", "--no-merges", "%s..%s" % (self.origin, committish)]):
commits.append(line.strip())
commits.reverse()
# and leaves CR at the end instead.
cr=$(printf "\015")
+empty_tree=$(git hash-object -t tree /dev/null)
+
strategy_args=${strategy:+--strategy=$strategy}
test -n "$strategy_opts" &&
eval '
append_todo_help () {
gettext "
Commands:
-p, pick = use commit
-r, reword = use commit, but edit the commit message
-e, edit = use commit, but stop for amending
-s, squash = use commit, but meld into previous commit
-f, fixup = like \"squash\", but discard this commit's log message
-x, exec = run command (the rest of the line) using shell
-d, drop = remove commit
+p, pick <commit> = use commit
+r, reword <commit> = use commit, but edit the commit message
+e, edit <commit> = use commit, but stop for amending
+s, squash <commit> = use commit, but meld into previous commit
+f, fixup <commit> = like \"squash\", but discard this commit's log message
+x, exec <commit> = run command (the rest of the line) using shell
+d, drop <commit> = remove commit
+l, label <label> = label current HEAD with a name
+t, reset <label> = reset HEAD to a label
+m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
+. create a merge commit using the original merge commit's
+. message (or the oneline, if no original merge commit was
+. specified). Use -c <commit> to reword the commit message.
These lines can be re-ordered; they are executed from top to bottom.
" | git stripspace --comment-lines >>"$todo"
die "$(eval_gettext "\$sha1: not a commit that can be picked")"
}
ptree=$(git rev-parse -q --verify "$1"^^{tree} 2>/dev/null) ||
- ptree=4b825dc642cb6eb9a060e54bf8d69288fbee4904
+ ptree=$empty_tree
test "$tree" = "$ptree"
}
else
revisions=$onto...$orig_head
shortrevisions=$shorthead
+ test -z "$squash_onto" ||
+ echo "$squash_onto" >"$state_dir"/squash-onto
fi
}
die "Could not skip unnecessary pick commands"
checkout_onto
- if test -z "$rebase_root" && test ! -d "$rewritten"
+ if test ! -d "$rewritten"
then
require_clean_work_tree "rebase"
exec git rebase--helper ${force_rebase:+--no-ff} $allow_empty_message \
init_revisions_and_shortrevisions
git rebase--helper --make-script ${keep_empty:+--keep-empty} \
+ ${rebase_merges:+--rebase-merges} \
+ ${rebase_cousins:+--rebase-cousins} \
$revisions ${restrict_revision+^$restrict_revision} >"$todo" ||
die "$(gettext "Could not generate todo list")"
autostash automatically stash/stash pop before and after
fork-point use 'merge-base --fork-point' to refine upstream
onto=! rebase onto given branch instead of upstream
+r,rebase-merges? try to rebase merges instead of skipping them
p,preserve-merges! try to recreate merges instead of ignoring them
s,strategy=! use the given merge strategy
no-ff! cherry-pick all commits, even if unchanged
state_dir=
# One of {'', continue, skip, abort}, as parsed from command line
action=
+rebase_merges=
+rebase_cousins=
preserve_merges=
autosquash=
keep_empty=
--no-keep-empty)
keep_empty=
;;
+ --rebase-merges)
+ rebase_merges=t
+ test -z "$interactive_rebase" && interactive_rebase=implied
+ ;;
+ --rebase-merges=*)
+ rebase_merges=t
+ case "${1#*=}" in
+ rebase-cousins) rebase_cousins=t;;
+ no-rebase-cousins) rebase_cousins=;;
+ *) die "Unknown mode: $1";;
+ esac
+ test -z "$interactive_rebase" && interactive_rebase=implied
+ ;;
--preserve-merges)
preserve_merges=t
test -z "$interactive_rebase" && interactive_rebase=implied
return File::Spec::Functions::file_name_is_absolute($path);
}
-# Returns 1 if the message was sent, and 0 otherwise.
-# In actuality, the whole program dies when there
-# is an error sending a message.
+# Prepares the email, then asks the user what to do.
+#
+# If the user chooses to send the email, it's sent and 1 is returned.
+# If the user chooses not to send the email, 0 is returned.
+# If the user decides they want to make further edits, -1 is returned and the
+# caller is expected to call send_message again after the edits are performed.
+#
+# If an error occurs sending the email, this just dies.
sub send_message {
my @recipients = unique_email_list(@to);
EOF
}
- # TRANSLATORS: Make sure to include [y] [n] [q] [a] in your
+ # TRANSLATORS: Make sure to include [y] [n] [e] [q] [a] in your
# translation. The program will only accept English input
# at this point.
- $_ = ask(__("Send this email? ([y]es|[n]o|[q]uit|[a]ll): "),
- valid_re => qr/^(?:yes|y|no|n|quit|q|all|a)/i,
+ $_ = ask(__("Send this email? ([y]es|[n]o|[e]dit|[q]uit|[a]ll): "),
+ valid_re => qr/^(?:yes|y|no|n|edit|e|quit|q|all|a)/i,
default => $ask_default);
die __("Send this email reply required") unless defined $_;
if (/^n/i) {
return 0;
+ } elsif (/^e/i) {
+ return -1;
} elsif (/^q/i) {
cleanup_compose_files();
exit(0);
$subject = $initial_subject;
$message_num = 0;
-foreach my $t (@files) {
+# Prepares the email, prompts the user, sends it out
+# Returns 0 if an edit was done and the function should be called again, or 1
+# otherwise.
+sub process_file {
+ my ($t) = @_;
+
open my $fh, "<", $t or die sprintf(__("can't open file %s"), $t);
my $author = undef;
}
my $message_was_sent = send_message();
+ if ($message_was_sent == -1) {
+ do_edit($t);
+ return 0;
+ }
# set up for the next message
if ($thread && $message_was_sent &&
undef $auth;
sleep($relogin_delay) if defined $relogin_delay;
}
+
+ return 1;
+}
+
+foreach my $t (@files) {
+ while (!process_file($t)) {
+ # user edited the file
+ }
}
# Execute a command (e.g. $to_cmd) to get a list of email addresses
custom_name=
depth=
progress=
+dissociate=
die_if_unmatched ()
{
-q|--quiet)
GIT_QUIET=1
;;
+ --progress)
+ progress=1
+ ;;
--reference)
case "$2" in '') usage ;; esac
reference_path=$2
--reference=*)
reference_path="${1#--reference=}"
;;
+ --dissociate)
+ dissociate=1
+ ;;
--name)
case "$2" in '') usage ;; esac
custom_name=$2
sm_name="$sm_path"
fi
+ if ! git submodule--helper check-name "$sm_name"
+ then
+ die "$(eval_gettext "'$sm_name' is not a valid submodule name")"
+ fi
+
# perhaps the path exists and is already a git repo, else clone it
if test -e "$sm_path"
then
eval_gettextln "Reactivating local git directory for submodule '\$sm_name'."
fi
fi
- git submodule--helper clone ${GIT_QUIET:+--quiet} --prefix "$wt_prefix" --path "$sm_path" --name "$sm_name" --url "$realrepo" ${reference:+"$reference"} ${depth:+"$depth"} || exit
+ git submodule--helper clone ${GIT_QUIET:+--quiet} ${progress:+"--progress"} --prefix "$wt_prefix" --path "$sm_path" --name "$sm_name" --url "$realrepo" ${reference:+"$reference"} ${dissociate:+"--dissociate"} ${depth:+"$depth"} || exit
(
sanitize_submodule_env
cd "$sm_path" &&
GIT_QUIET=1
;;
--progress)
- progress="--progress"
+ progress=1
;;
-i|--init)
init=1
--reference=*)
reference="$1"
;;
+ --dissociate)
+ dissociate=1
+ ;;
-m|--merge)
update="merge"
;;
{
git submodule--helper update-clone ${GIT_QUIET:+--quiet} \
- ${progress:+"$progress"} \
+ ${progress:+"--progress"} \
${wt_prefix:+--prefix "$wt_prefix"} \
${prefix:+--recursive-prefix "$prefix"} \
${update:+--update "$update"} \
${reference:+"$reference"} \
+ ${dissociate:+"--dissociate"} \
${depth:+--depth "$depth"} \
- ${recommend_shallow:+"$recommend_shallow"} \
- ${jobs:+$jobs} \
+ $recommend_shallow \
+ $jobs \
"$@" || echo "#unmatched" $?
} | {
err=
# is not reachable from a ref.
is_tip_reachable "$sm_path" "$sha1" ||
fetch_in_submodule "$sm_path" $depth ||
- die "$(eval_gettext "Unable to fetch in submodule path '\$displaypath'")"
+ say "$(eval_gettext "Unable to fetch in submodule path '\$displaypath'")"
# Now we tried the usual fetch, but $sha1 may
# not be reachable from any of the refs
const char git_usage_string[] =
N_("git [--version] [--help] [-C <path>] [-c <name>=<value>]\n"
" [--exec-path[=<path>]] [--html-path] [--man-path] [--info-path]\n"
- " [-p | --paginate | --no-pager] [--no-replace-objects] [--bare]\n"
+ " [-p | --paginate | -P | --no-pager] [--no-replace-objects] [--bare]\n"
" [--git-dir=<path>] [--work-tree=<path>] [--namespace=<name>]\n"
" <command> [<args>]");
exit(0);
} else if (!strcmp(cmd, "-p") || !strcmp(cmd, "--paginate")) {
use_pager = 1;
- } else if (!strcmp(cmd, "--no-pager")) {
+ } else if (!strcmp(cmd, "-P") || !strcmp(cmd, "--no-pager")) {
use_pager = 0;
if (envchanged)
*envchanged = 1;
die("Couldn't allocate PCRE JIT stack");
pcre_assign_jit_stack(p->pcre1_extra_info, NULL, p->pcre1_jit_stack);
} else if (p->pcre1_jit_on != 0) {
- die("BUG: The pcre1_jit_on variable should be 0 or 1, not %d",
+ BUG("The pcre1_jit_on variable should be 0 or 1, not %d",
p->pcre1_jit_on);
}
#endif
die("Couldn't allocate PCRE2 match context");
pcre2_jit_stack_assign(p->pcre2_match_context, NULL, p->pcre2_jit_stack);
} else if (p->pcre2_jit_on != 0) {
- die("BUG: The pcre2_jit_on variable should be 0 or 1, not %d",
+ BUG("The pcre2_jit_on variable should be 0 or 1, not %d",
p->pcre1_jit_on);
}
}
for (p = opt->header_list; p; p = p->next) {
if (p->token != GREP_PATTERN_HEAD)
- die("BUG: a non-header pattern in grep header list.");
+ BUG("a non-header pattern in grep header list.");
if (p->field < GREP_HEADER_FIELD_MIN ||
GREP_HEADER_FIELD_MAX <= p->field)
- die("BUG: unknown header field %d", p->field);
+ BUG("unknown header field %d", p->field);
compile_regexp(p, opt);
}
h = compile_pattern_atom(&pp);
if (!h || pp != p->next)
- die("BUG: malformed header expr");
+ BUG("malformed header expr");
if (!header_group[p->field]) {
header_group[p->field] = h;
continue;
fill_filespec(df, &null_oid, 0, 0100644);
break;
default:
- die("BUG: attempt to textconv something without a path?");
+ BUG("attempt to textconv something without a path?");
}
/*
case GREP_BINARY_TEXT:
break;
default:
- die("BUG: unknown binary handling mode");
+ BUG("unknown binary handling mode");
}
}
case GREP_SOURCE_BUF:
return gs->buf ? 0 : -1;
}
- die("BUG: invalid grep_source type to load");
+ BUG("invalid grep_source type to load");
}
void grep_source_load_driver(struct grep_source *gs)
int count = 0;
while ((commit = get_revision(revs)) != NULL) {
- p = process_tree(commit->tree, p);
+ p = process_tree(get_commit_tree(commit), p);
commit->object.flags |= LOCAL;
if (!(commit->object.flags & UNINTERESTING))
count += add_send_request(&commit->object, lock);
{
struct transfer_request *request;
struct transfer_request *next_request;
- int nr_refspec = 0;
- const char **refspec = NULL;
+ struct refspec rs = REFSPEC_INIT_PUSH;
struct remote_lock *ref_lock = NULL;
struct remote_lock *info_ref_lock = NULL;
struct rev_info revs;
}
continue;
}
- refspec = argv;
- nr_refspec = argc - i;
+ refspec_appendn(&rs, argv, argc - i);
break;
}
if (!repo->url)
usage(http_push_usage);
- if (delete_branch && nr_refspec != 1)
+ if (delete_branch && rs.nr != 1)
die("You must specify only one branch name when deleting a remote branch");
setup_git_directory();
/* Remove a remote branch if -d or -D was specified */
if (delete_branch) {
- if (delete_remote_branch(refspec[0], force_delete) == -1) {
+ const char *branch = rs.items[i].src;
+ if (delete_remote_branch(branch, force_delete) == -1) {
fprintf(stderr, "Unable to delete remote branch %s\n",
- refspec[0]);
+ branch);
if (helper_status)
- printf("error %s cannot remove\n", refspec[0]);
+ printf("error %s cannot remove\n", branch);
}
goto cleanup;
}
/* match them up */
- if (match_push_refs(local_refs, &remote_refs,
- nr_refspec, (const char **) refspec, push_all)) {
+ if (match_push_refs(local_refs, &remote_refs, &rs, push_all)) {
rc = -1;
goto cleanup;
}
return 0;
if (!skip_prefix(asked, base->buf, &tail))
- die("BUG: update_url_from_redirect: %s is not a superset of %s",
+ BUG("update_url_from_redirect: %s is not a superset of %s",
asked, base->buf);
new_len = got->len;
strbuf_reset(result);
break;
default:
- die("BUG: HTTP_KEEP_ERROR is only supported with strbufs");
+ BUG("HTTP_KEEP_ERROR is only supported with strbufs");
}
}
int ret = 0, i = 0;
char *url, *data;
struct strbuf buf = STRBUF_INIT;
- unsigned char sha1[20];
+ unsigned char hash[GIT_MAX_RAWSZ];
+ const unsigned hexsz = the_hash_algo->hexsz;
end_url_with_slash(&buf, base_url);
strbuf_addstr(&buf, "objects/info/packs");
switch (data[i]) {
case 'P':
i++;
- if (i + 52 <= buf.len &&
+ if (i + hexsz + 12 <= buf.len &&
starts_with(data + i, " pack-") &&
- starts_with(data + i + 46, ".pack\n")) {
- get_sha1_hex(data + i + 6, sha1);
- fetch_and_setup_pack_index(packs_head, sha1,
+ starts_with(data + i + hexsz + 6, ".pack\n")) {
+ get_sha1_hex(data + i + 6, hash);
+ fetch_and_setup_pack_index(packs_head, hash,
base_url);
- i += 51;
+ i += hexsz + 11;
break;
}
default:
*lst = (*lst)->next;
if (!strip_suffix(preq->tmpfile, ".pack.temp", &len))
- die("BUG: pack tmpfile does not end in .pack.temp?");
+ BUG("pack tmpfile does not end in .pack.temp?");
tmp_idx = xstrfmt("%.*s.idx.temp", (int)len, preq->tmpfile);
argv_array_push(&ip.args, "index-pack");
CURLcode c = curl_easy_getinfo(slot->curl, CURLINFO_HTTP_CODE,
&slot->http_code);
if (c != CURLE_OK)
- die("BUG: curl_easy_getinfo for HTTP code failed: %s",
+ BUG("curl_easy_getinfo for HTTP code failed: %s",
curl_easy_strerror(c));
if (slot->http_code >= 300)
return size;
va_start(va, fmt);
if (blen <= 0 || (unsigned)(ret = vsnprintf(buf, blen, fmt, va)) >= (unsigned)blen)
- die("BUG: buffer too small. Please report a bug.");
+ BUG("buffer too small. Please report a bug.");
va_end(va);
return ret;
}
assert(commit);
DIFF_QUEUE_CLEAR(&diff_queued_diff);
- diff_tree_oid(parent ? &parent->tree->object.oid : NULL,
- &commit->tree->object.oid, "", opt);
+ diff_tree_oid(parent ? get_commit_tree_oid(parent) : NULL,
+ get_commit_tree_oid(commit), "", opt);
if (opt->detect_rename) {
filter_diffs_for_paths(range, 1);
if (diff_might_be_rename())
assert(obj->type == OBJ_BLOB);
assert((obj->flags & SEEN) == 0);
- t = oid_object_info(&obj->oid, &object_length);
+ t = oid_object_info(the_repository, &obj->oid, &object_length);
if (t != OBJ_BLOB) { /* probably OBJ_NONE */
/*
* We DO NOT have the blob locally, so we cannot
struct commit *parent = parents->item;
if (!(parent->object.flags & UNINTERESTING))
continue;
- mark_tree_uninteresting(parent->tree);
+ mark_tree_uninteresting(get_commit_tree(parent));
if (revs->edge_hint && !(parent->object.flags & SHOWN)) {
parent->object.flags |= SHOWN;
show_edge(parent);
struct commit *commit = list->item;
if (commit->object.flags & UNINTERESTING) {
- mark_tree_uninteresting(commit->tree);
+ mark_tree_uninteresting(get_commit_tree(commit));
if (revs->edge_hint_aggressive && !(commit->object.flags & SHOWN)) {
commit->object.flags |= SHOWN;
show_edge(commit);
struct commit *commit = (struct commit *)obj;
if (obj->type != OBJ_COMMIT || !(obj->flags & UNINTERESTING))
continue;
- mark_tree_uninteresting(commit->tree);
+ mark_tree_uninteresting(get_commit_tree(commit));
if (!(obj->flags & SHOWN)) {
obj->flags |= SHOWN;
show_edge(commit);
* an uninteresting boundary commit may not have its tree
* parsed yet, but we are not going to show them anyway
*/
- if (commit->tree)
- add_pending_tree(revs, commit->tree);
+ if (get_commit_tree(commit))
+ add_pending_tree(revs, get_commit_tree(commit));
show_commit(commit, show_data);
if (revs->tree_blobs_in_commit_order)
strbuf_addstr(&ret, get_tempfile_path(lk->tempfile));
if (ret.len <= LOCK_SUFFIX_LEN ||
strcmp(ret.buf + ret.len - LOCK_SUFFIX_LEN, LOCK_SUFFIX))
- die("BUG: get_locked_file_path() called for malformed lock object");
+ BUG("get_locked_file_path() called for malformed lock object");
/* remove ".lock": */
strbuf_setlen(&ret, ret.len - LOCK_SUFFIX_LEN);
return strbuf_detach(&ret, NULL);
void log_write_email_headers(struct rev_info *opt, struct commit *commit,
const char **extra_headers_p,
- int *need_8bit_cte_p)
+ int *need_8bit_cte_p,
+ int maybe_multipart)
{
const char *extra_headers = opt->extra_headers;
const char *name = oid_to_hex(opt->zero_commit ?
opt->ref_message_ids->items[i].string);
graph_show_oneline(opt->graph);
}
- if (opt->mime_boundary) {
+ if (opt->mime_boundary && maybe_multipart) {
static char subject_buffer[1024];
static char buffer[1024];
struct strbuf filename = STRBUF_INIT;
&& !commit->parents->next->next);
}
-static void show_one_mergetag(struct commit *commit,
- struct commit_extra_header *extra,
- void *data)
+static int show_one_mergetag(struct commit *commit,
+ struct commit_extra_header *extra,
+ void *data)
{
struct rev_info *opt = (struct rev_info *)data;
struct object_id oid;
hash_object_file(extra->value, extra->len, type_name(OBJ_TAG), &oid);
tag = lookup_tag(&oid);
if (!tag)
- return; /* error message already given */
+ return -1; /* error message already given */
strbuf_init(&verify_message, 256);
if (parse_tag_buffer(tag, extra->value, extra->len))
show_sig_lines(opt, status, verify_message.buf);
strbuf_release(&verify_message);
+ return 0;
}
-static void show_mergetag(struct rev_info *opt, struct commit *commit)
+static int show_mergetag(struct rev_info *opt, struct commit *commit)
{
- for_each_mergetag(show_one_mergetag, commit, opt);
+ return for_each_mergetag(show_one_mergetag, commit, opt);
}
void show_log(struct rev_info *opt)
if (cmit_fmt_is_mail(opt->commit_format)) {
log_write_email_headers(opt, commit, &extra_headers,
- &ctx.need_8bit_cte);
+ &ctx.need_8bit_cte, 1);
ctx.rev = opt;
ctx.print_email_subject = 1;
} else if (opt->commit_format != CMIT_FMT_USERFORMAT) {
return 0;
parse_commit_or_die(commit);
- oid = &commit->tree->object.oid;
+ oid = get_commit_tree_oid(commit);
/* Root commit? */
parents = get_saved_parents(opt, commit);
* we merged _in_.
*/
parse_commit_or_die(parents->item);
- diff_tree_oid(&parents->item->tree->object.oid,
+ diff_tree_oid(get_commit_tree_oid(parents->item),
oid, "", &opt->diffopt);
log_tree_diff_flush(opt);
return !opt->loginfo;
struct commit *parent = parents->item;
parse_commit_or_die(parent);
- diff_tree_oid(&parent->tree->object.oid,
+ diff_tree_oid(get_commit_tree_oid(parent),
oid, "", &opt->diffopt);
log_tree_diff_flush(opt);
void show_decorations(struct rev_info *opt, struct commit *commit);
void log_write_email_headers(struct rev_info *opt, struct commit *commit,
const char **extra_headers_p,
- int *need_8bit_cte_p);
+ int *need_8bit_cte_p,
+ int maybe_multipart);
void load_ref_decorations(struct decoration_filter *filter, int flags);
#define FORMAT_PATCH_NAME_MAX 64
if (!mi->inbody_header_accum.len)
return;
if (!check_header(mi, &mi->inbody_header_accum, mi->s_hdr_data, 0))
- die("BUG: inbody_header_accum, if not empty, must always contain a valid in-body header");
+ BUG("inbody_header_accum, if not empty, must always contain a valid in-body header");
strbuf_reset(&mi->inbody_header_accum);
}
return ignore_case ? strihash(path) : strhash(path);
}
+static struct dir_rename_entry *dir_rename_find_entry(struct hashmap *hashmap,
+ char *dir)
+{
+ struct dir_rename_entry key;
+
+ if (dir == NULL)
+ return NULL;
+ hashmap_entry_init(&key, strhash(dir));
+ key.dir = dir;
+ return hashmap_get(hashmap, &key, NULL);
+}
+
+static int dir_rename_cmp(const void *unused_cmp_data,
+ const void *entry,
+ const void *entry_or_key,
+ const void *unused_keydata)
+{
+ const struct dir_rename_entry *e1 = entry;
+ const struct dir_rename_entry *e2 = entry_or_key;
+
+ return strcmp(e1->dir, e2->dir);
+}
+
+static void dir_rename_init(struct hashmap *map)
+{
+ hashmap_init(map, dir_rename_cmp, NULL, 0);
+}
+
+static void dir_rename_entry_init(struct dir_rename_entry *entry,
+ char *directory)
+{
+ hashmap_entry_init(entry, strhash(directory));
+ entry->dir = directory;
+ entry->non_unique_new_dir = 0;
+ strbuf_init(&entry->new_dir, 0);
+ string_list_init(&entry->possible_new_dirs, 0);
+}
+
+static struct collision_entry *collision_find_entry(struct hashmap *hashmap,
+ char *target_file)
+{
+ struct collision_entry key;
+
+ hashmap_entry_init(&key, strhash(target_file));
+ key.target_file = target_file;
+ return hashmap_get(hashmap, &key, NULL);
+}
+
+static int collision_cmp(void *unused_cmp_data,
+ const struct collision_entry *e1,
+ const struct collision_entry *e2,
+ const void *unused_keydata)
+{
+ return strcmp(e1->target_file, e2->target_file);
+}
+
+static void collision_init(struct hashmap *map)
+{
+ hashmap_init(map, (hashmap_cmp_fn) collision_cmp, NULL, 0);
+}
+
static void flush_output(struct merge_options *o)
{
if (o->buffer_output < 2 && o->obuf.len) {
struct commit *commit = alloc_commit_node();
set_merge_remote_desc(commit, comment, (struct object *)commit);
- commit->tree = tree;
+ commit->maybe_tree = tree;
commit->object.parsed = 1;
return commit;
}
enum rename_type {
RENAME_NORMAL = 0,
+ RENAME_DIR,
RENAME_DELETE,
RENAME_ONE_FILE_TO_ONE,
RENAME_ONE_FILE_TO_TWO,
ce = make_cache_entry(mode, oid ? oid->hash : null_sha1, path, stage, 0);
if (!ce)
- return err(o, _("addinfo_cache failed for path '%s'"), path);
+ return err(o, _("add_cacheinfo failed for path '%s'; merge aborting."), path);
ret = add_cache_entry(ce, options);
if (refresh) {
nce = refresh_cache_entry(ce, CE_MATCH_REFRESH | CE_MATCH_IGNORE_MISSING);
if (!nce)
- return err(o, _("addinfo_cache failed for path '%s'"), path);
+ return err(o, _("add_cacheinfo failed to refresh for path '%s'; merge aborting."), path);
if (nce != ce)
ret = add_cache_entry(nce, options);
}
init_tree_desc(desc, tree->buffer, tree->size);
}
-static int git_merge_trees(int index_only,
+static int git_merge_trees(struct merge_options *o,
struct tree *common,
struct tree *head,
struct tree *merge)
{
int rc;
struct tree_desc t[3];
- struct unpack_trees_options opts;
+ struct index_state tmp_index = { NULL };
- memset(&opts, 0, sizeof(opts));
- if (index_only)
- opts.index_only = 1;
+ memset(&o->unpack_opts, 0, sizeof(o->unpack_opts));
+ if (o->call_depth)
+ o->unpack_opts.index_only = 1;
else
- opts.update = 1;
- opts.merge = 1;
- opts.head_idx = 2;
- opts.fn = threeway_merge;
- opts.src_index = &the_index;
- opts.dst_index = &the_index;
- setup_unpack_trees_porcelain(&opts, "merge");
+ o->unpack_opts.update = 1;
+ o->unpack_opts.merge = 1;
+ o->unpack_opts.head_idx = 2;
+ o->unpack_opts.fn = threeway_merge;
+ o->unpack_opts.src_index = &the_index;
+ o->unpack_opts.dst_index = &tmp_index;
+ o->unpack_opts.aggressive = !merge_detect_rename(o);
+ setup_unpack_trees_porcelain(&o->unpack_opts, "merge");
init_tree_desc_from_tree(t+0, common);
init_tree_desc_from_tree(t+1, head);
init_tree_desc_from_tree(t+2, merge);
- rc = unpack_trees(3, t, &opts);
+ rc = unpack_trees(3, t, &o->unpack_opts);
cache_tree_free(&active_cache_tree);
+
+ /*
+ * Update the_index to match the new results, AFTER saving a copy
+ * in o->orig_index. Update src_index to point to the saved copy.
+ * (verify_uptodate() checks src_index, and the original index is
+ * the one that had the necessary modification timestamps.)
+ */
+ o->orig_index = the_index;
+ the_index = tmp_index;
+ o->unpack_opts.src_index = &o->orig_index;
+
return rc;
}
fprintf(stderr, "BUG: %d %.*s\n", ce_stage(ce),
(int)ce_namelen(ce), ce->name);
}
- die("BUG: unmerged index entries in merge-recursive.c");
+ BUG("unmerged index entries in merge-recursive.c");
}
if (!active_cache_tree)
read_tree_recursive(tree, "", 0, 0, &match_all, save_files_dirs, o);
}
+static int get_tree_entry_if_blob(const struct object_id *tree,
+ const char *path,
+ struct object_id *hashy,
+ unsigned int *mode_o)
+{
+ int ret;
+
+ ret = get_tree_entry(tree, path, hashy, mode_o);
+ if (S_ISDIR(*mode_o)) {
+ oidcpy(hashy, &null_oid);
+ *mode_o = 0;
+ }
+ return ret;
+}
+
/*
* Returns an index_entry instance which doesn't have to correspond to
* a real cache entry in Git's index.
{
struct string_list_item *item;
struct stage_data *e = xcalloc(1, sizeof(struct stage_data));
- get_tree_entry(&o->object.oid, path,
- &e->stages[1].oid, &e->stages[1].mode);
- get_tree_entry(&a->object.oid, path,
- &e->stages[2].oid, &e->stages[2].mode);
- get_tree_entry(&b->object.oid, path,
- &e->stages[3].oid, &e->stages[3].mode);
+ get_tree_entry_if_blob(&o->object.oid, path,
+ &e->stages[1].oid, &e->stages[1].mode);
+ get_tree_entry_if_blob(&a->object.oid, path,
+ &e->stages[2].oid, &e->stages[2].mode);
+ get_tree_entry_if_blob(&b->object.oid, path,
+ &e->stages[3].oid, &e->stages[3].mode);
item = string_list_insert(entries, path);
item->util = e;
return e;
*/
struct stage_data *src_entry;
struct stage_data *dst_entry;
+ unsigned add_turned_into_rename:1;
unsigned processed:1;
};
-/*
- * Get information of all renames which occurred between 'o_tree' and
- * 'tree'. We need the three trees in the merge ('o_tree', 'a_tree' and
- * 'b_tree') to be able to associate the correct cache entries with
- * the rename information. 'tree' is always equal to either a_tree or b_tree.
- */
-static struct string_list *get_renames(struct merge_options *o,
- struct tree *tree,
- struct tree *o_tree,
- struct tree *a_tree,
- struct tree *b_tree,
- struct string_list *entries)
-{
- int i;
- struct string_list *renames;
- struct diff_options opts;
-
- renames = xcalloc(1, sizeof(struct string_list));
- if (!o->detect_rename)
- return renames;
-
- diff_setup(&opts);
- opts.flags.recursive = 1;
- opts.flags.rename_empty = 0;
- opts.detect_rename = DIFF_DETECT_RENAME;
- opts.rename_limit = o->merge_rename_limit >= 0 ? o->merge_rename_limit :
- o->diff_rename_limit >= 0 ? o->diff_rename_limit :
- 1000;
- opts.rename_score = o->rename_score;
- opts.show_rename_progress = o->show_rename_progress;
- opts.output_format = DIFF_FORMAT_NO_OUTPUT;
- diff_setup_done(&opts);
- diff_tree_oid(&o_tree->object.oid, &tree->object.oid, "", &opts);
- diffcore_std(&opts);
- if (opts.needed_rename_limit > o->needed_rename_limit)
- o->needed_rename_limit = opts.needed_rename_limit;
- for (i = 0; i < diff_queued_diff.nr; ++i) {
- struct string_list_item *item;
- struct rename *re;
- struct diff_filepair *pair = diff_queued_diff.queue[i];
- if (pair->status != 'R') {
- diff_free_filepair(pair);
- continue;
- }
- re = xmalloc(sizeof(*re));
- re->processed = 0;
- re->pair = pair;
- item = string_list_lookup(entries, re->pair->one->path);
- if (!item)
- re->src_entry = insert_stage_data(re->pair->one->path,
- o_tree, a_tree, b_tree, entries);
- else
- re->src_entry = item->util;
-
- item = string_list_lookup(entries, re->pair->two->path);
- if (!item)
- re->dst_entry = insert_stage_data(re->pair->two->path,
- o_tree, a_tree, b_tree, entries);
- else
- re->dst_entry = item->util;
- item = string_list_insert(renames, pair->one->path);
- item->util = re;
- }
- opts.output_format = DIFF_FORMAT_NO_OUTPUT;
- diff_queued_diff.nr = 0;
- diff_flush(&opts);
- return renames;
-}
-
static int update_stages(struct merge_options *opt, const char *path,
const struct diff_filespec *o,
const struct diff_filespec *a,
return 0;
}
+static int update_stages_for_stage_data(struct merge_options *opt,
+ const char *path,
+ const struct stage_data *stage_data)
+{
+ struct diff_filespec o, a, b;
+
+ o.mode = stage_data->stages[1].mode;
+ oidcpy(&o.oid, &stage_data->stages[1].oid);
+
+ a.mode = stage_data->stages[2].mode;
+ oidcpy(&a.oid, &stage_data->stages[2].oid);
+
+ b.mode = stage_data->stages[3].mode;
+ oidcpy(&b.oid, &stage_data->stages[3].oid);
+
+ return update_stages(opt, path,
+ is_null_oid(&o.oid) ? NULL : &o,
+ is_null_oid(&a.oid) ? NULL : &a,
+ is_null_oid(&b.oid) ? NULL : &b);
+}
+
static void update_entry(struct stage_data *entry,
struct diff_filespec *o,
struct diff_filespec *a,
!(empty_ok && is_empty_dir(path));
}
-static int was_tracked(const char *path)
+/*
+ * Returns whether path was tracked in the index before the merge started,
+ * and its oid and mode match the specified values
+ */
+static int was_tracked_and_matches(struct merge_options *o, const char *path,
+ const struct object_id *oid, unsigned mode)
{
- int pos = cache_name_pos(path, strlen(path));
+ int pos = index_name_pos(&o->orig_index, path, strlen(path));
+ struct cache_entry *ce;
+
+ if (0 > pos)
+ /* we were not tracking this path before the merge */
+ return 0;
+
+ /* See if the file we were tracking before matches */
+ ce = o->orig_index.cache[pos];
+ return (oid_eq(&ce->oid, oid) && ce->ce_mode == mode);
+}
+
+/*
+ * Returns whether path was tracked in the index before the merge started
+ */
+static int was_tracked(struct merge_options *o, const char *path)
+{
+ int pos = index_name_pos(&o->orig_index, path, strlen(path));
if (0 <= pos)
- /* we have been tracking this path */
+ /* we were tracking this path before the merge */
return 1;
- /*
- * Look for an unmerged entry for the path,
- * specifically stage #2, which would indicate
- * that "our" side before the merge started
- * had the path tracked (and resulted in a conflict).
- */
- for (pos = -1 - pos;
- pos < active_nr && !strcmp(path, active_cache[pos]->name);
- pos++)
- if (ce_stage(active_cache[pos]) == 2)
- return 1;
return 0;
}
static int would_lose_untracked(const char *path)
{
- return !was_tracked(path) && file_exists(path);
+ /*
+ * This may look like it can be simplified to:
+ * return !was_tracked(o, path) && file_exists(path)
+ * but it can't. This function needs to know whether path was in
+ * the working tree due to EITHER having been tracked in the index
+ * before the merge OR having been put into the working copy and
+ * index by unpack_trees(). Due to that either-or requirement, we
+ * check the current index instead of the original one.
+ *
+ * Note that we do not need to worry about merge-recursive itself
+ * updating the index after unpack_trees() and before calling this
+ * function, because we strictly require all code paths in
+ * merge-recursive to update the working tree first and the index
+ * second. Doing otherwise would break
+ * update_file()/would_lose_untracked(); see every comment in this
+ * file which mentions "update_stages".
+ */
+ int pos = cache_name_pos(path, strlen(path));
+
+ if (pos < 0)
+ pos = -1 - pos;
+ while (pos < active_nr &&
+ !strcmp(path, active_cache[pos]->name)) {
+ /*
+ * If stage #0, it is definitely tracked.
+ * If it has stage #2 then it was tracked
+ * before this merge started. All other
+ * cases the path was not tracked.
+ */
+ switch (ce_stage(active_cache[pos])) {
+ case 0:
+ case 2:
+ return 0;
+ }
+ pos++;
+ }
+ return file_exists(path);
+}
+
+static int was_dirty(struct merge_options *o, const char *path)
+{
+ struct cache_entry *ce;
+ int dirty = 1;
+
+ if (o->call_depth || !was_tracked(o, path))
+ return !dirty;
+
+ ce = index_file_exists(o->unpack_opts.src_index,
+ path, strlen(path), ignore_case);
+ dirty = verify_uptodate(ce, &o->unpack_opts) != 0;
+ return dirty;
}
static int make_room_for_path(struct merge_options *o, const char *path)
}
update_index:
if (!ret && update_cache)
- add_cacheinfo(o, mode, oid, path, 0, update_wd, ADD_CACHE_OK_TO_ADD);
+ if (add_cacheinfo(o, mode, oid, path, 0, update_wd,
+ ADD_CACHE_OK_TO_ADD))
+ return -1;
return ret;
}
}
static int merge_file_1(struct merge_options *o,
- const struct diff_filespec *one,
- const struct diff_filespec *a,
- const struct diff_filespec *b,
- const char *branch1,
- const char *branch2,
- struct merge_file_info *result)
+ const struct diff_filespec *one,
+ const struct diff_filespec *a,
+ const struct diff_filespec *b,
+ const char *filename,
+ const char *branch1,
+ const char *branch2,
+ struct merge_file_info *result)
{
result->merge = 0;
result->clean = 1;
break;
}
} else
- die("BUG: unsupported object type in the tree");
+ BUG("unsupported object type in the tree");
}
+ if (result->merge)
+ output(o, 2, _("Auto-merging %s"), filename);
+
return 0;
}
static int merge_file_special_markers(struct merge_options *o,
- const struct diff_filespec *one,
- const struct diff_filespec *a,
- const struct diff_filespec *b,
- const char *branch1,
- const char *filename1,
- const char *branch2,
- const char *filename2,
- struct merge_file_info *mfi)
+ const struct diff_filespec *one,
+ const struct diff_filespec *a,
+ const struct diff_filespec *b,
+ const char *target_filename,
+ const char *branch1,
+ const char *filename1,
+ const char *branch2,
+ const char *filename2,
+ struct merge_file_info *mfi)
{
char *side1 = NULL;
char *side2 = NULL;
if (filename2)
side2 = xstrfmt("%s:%s", branch2, filename2);
- ret = merge_file_1(o, one, a, b,
+ ret = merge_file_1(o, one, a, b, target_filename,
side1 ? side1 : branch1,
side2 ? side2 : branch2, mfi);
+
free(side1);
free(side2);
return ret;
}
static int merge_file_one(struct merge_options *o,
- const char *path,
- const struct object_id *o_oid, int o_mode,
- const struct object_id *a_oid, int a_mode,
- const struct object_id *b_oid, int b_mode,
- const char *branch1,
- const char *branch2,
- struct merge_file_info *mfi)
+ const char *path,
+ const struct object_id *o_oid, int o_mode,
+ const struct object_id *a_oid, int a_mode,
+ const struct object_id *b_oid, int b_mode,
+ const char *branch1,
+ const char *branch2,
+ struct merge_file_info *mfi)
{
struct diff_filespec one, a, b;
a.mode = a_mode;
oidcpy(&b.oid, b_oid);
b.mode = b_mode;
- return merge_file_1(o, &one, &a, &b, branch1, branch2, mfi);
+ return merge_file_1(o, &one, &a, &b, path, branch1, branch2, mfi);
+}
+
+static int conflict_rename_dir(struct merge_options *o,
+ struct diff_filepair *pair,
+ const char *rename_branch,
+ const char *other_branch)
+{
+ const struct diff_filespec *dest = pair->two;
+
+ if (!o->call_depth && would_lose_untracked(dest->path)) {
+ char *alt_path = unique_path(o, dest->path, rename_branch);
+
+ output(o, 1, _("Error: Refusing to lose untracked file at %s; "
+ "writing to %s instead."),
+ dest->path, alt_path);
+ /*
+ * Write the file in worktree at alt_path, but not in the
+ * index. Instead, write to dest->path for the index but
+ * only at the higher appropriate stage.
+ */
+ if (update_file(o, 0, &dest->oid, dest->mode, alt_path))
+ return -1;
+ free(alt_path);
+ return update_stages(o, dest->path, NULL,
+ rename_branch == o->branch1 ? dest : NULL,
+ rename_branch == o->branch1 ? NULL : dest);
+ }
+
+ /* Update dest->path both in index and in worktree */
+ if (update_file(o, 1, &dest->oid, dest->mode, dest->path))
+ return -1;
+ return 0;
}
static int handle_change_delete(struct merge_options *o,
const char *update_path = path;
int ret = 0;
- if (dir_in_way(path, !o->call_depth, 0)) {
+ if (dir_in_way(path, !o->call_depth, 0) ||
+ (!o->call_depth && would_lose_untracked(path))) {
update_path = alt_path = unique_path(o, path, change_branch);
}
add = filespec_from_entry(&other, dst_entry, stage ^ 1);
if (add) {
+ int ren_src_was_dirty = was_dirty(o, rename->path);
char *add_name = unique_path(o, rename->path, other_branch);
if (update_file(o, 0, &add->oid, add->mode, add_name))
return -1;
- remove_file(o, 0, rename->path, 0);
+ if (ren_src_was_dirty) {
+ output(o, 1, _("Refusing to lose dirty file at %s"),
+ rename->path);
+ }
+ /*
+ * Because the double negatives somehow keep confusing me...
+ * 1) update_wd iff !ren_src_was_dirty.
+ * 2) no_wd iff !update_wd
+ * 3) so, no_wd == !!ren_src_was_dirty == ren_src_was_dirty
+ */
+ remove_file(o, 0, rename->path, ren_src_was_dirty);
dst_name = unique_path(o, rename->path, cur_branch);
} else {
if (dir_in_way(rename->path, !o->call_depth, 0)) {
dst_name = unique_path(o, rename->path, cur_branch);
output(o, 1, _("%s is a directory in %s adding as %s instead"),
rename->path, other_branch, dst_name);
+ } else if (!o->call_depth &&
+ would_lose_untracked(rename->path)) {
+ dst_name = unique_path(o, rename->path, cur_branch);
+ output(o, 1, _("Refusing to lose untracked file at %s; "
+ "adding as %s instead"),
+ rename->path, dst_name);
}
}
if ((ret = update_file(o, 0, &rename->oid, rename->mode, dst_name)))
struct diff_filespec *c1 = ci->pair1->two;
struct diff_filespec *c2 = ci->pair2->two;
char *path = c1->path; /* == c2->path */
+ char *path_side_1_desc;
+ char *path_side_2_desc;
struct merge_file_info mfi_c1;
struct merge_file_info mfi_c2;
int ret;
remove_file(o, 1, a->path, o->call_depth || would_lose_untracked(a->path));
remove_file(o, 1, b->path, o->call_depth || would_lose_untracked(b->path));
+ path_side_1_desc = xstrfmt("%s (was %s)", path, a->path);
+ path_side_2_desc = xstrfmt("%s (was %s)", path, b->path);
if (merge_file_special_markers(o, a, c1, &ci->ren1_other,
+ path_side_1_desc,
o->branch1, c1->path,
o->branch2, ci->ren1_other.path, &mfi_c1) ||
merge_file_special_markers(o, b, &ci->ren2_other, c2,
+ path_side_2_desc,
o->branch1, ci->ren2_other.path,
o->branch2, c2->path, &mfi_c2))
return -1;
+ free(path_side_1_desc);
+ free(path_side_2_desc);
if (o->call_depth) {
/*
char *new_path2 = unique_path(o, path, ci->branch2);
output(o, 1, _("Renaming %s to %s and %s to %s instead"),
a->path, new_path1, b->path, new_path2);
- remove_file(o, 0, path, 0);
+ if (was_dirty(o, path))
+ output(o, 1, _("Refusing to lose dirty file at %s"),
+ path);
+ else if (would_lose_untracked(path))
+ /*
+ * Only way we get here is if both renames were from
+ * a directory rename AND user had an untracked file
+ * at the location where both files end up after the
+ * two directory renames. See testcase 10d of t6043.
+ */
+ output(o, 1, _("Refusing to lose untracked file at "
+ "%s, even though it's in the way."),
+ path);
+ else
+ remove_file(o, 0, path, 0);
ret = update_file(o, 0, &mfi_c1.oid, mfi_c1.mode, new_path1);
if (!ret)
ret = update_file(o, 0, &mfi_c2.oid, mfi_c2.mode,
new_path2);
+ /*
+ * unpack_trees() actually populates the index for us for
+ * "normal" rename/rename(2to1) situtations so that the
+ * correct entries are at the higher stages, which would
+ * make the call below to update_stages_for_stage_data
+ * unnecessary. However, if either of the renames came
+ * from a directory rename, then unpack_trees() will not
+ * have gotten the right data loaded into the index, so we
+ * need to do so now. (While it'd be tempting to move this
+ * call to update_stages_for_stage_data() to
+ * apply_directory_rename_modifications(), that would break
+ * our intermediate calls to would_lose_untracked() since
+ * those rely on the current in-memory index. See also the
+ * big "NOTE" in update_stages()).
+ */
+ if (update_stages_for_stage_data(o, path, ci->dst_entry1))
+ ret = -1;
+
free(new_path2);
free(new_path1);
}
return ret;
}
+/*
+ * Get the diff_filepairs changed between o_tree and tree.
+ */
+static struct diff_queue_struct *get_diffpairs(struct merge_options *o,
+ struct tree *o_tree,
+ struct tree *tree)
+{
+ struct diff_queue_struct *ret;
+ struct diff_options opts;
+
+ diff_setup(&opts);
+ opts.flags.recursive = 1;
+ opts.flags.rename_empty = 0;
+ opts.detect_rename = merge_detect_rename(o);
+ /*
+ * We do not have logic to handle the detection of copies. In
+ * fact, it may not even make sense to add such logic: would we
+ * really want a change to a base file to be propagated through
+ * multiple other files by a merge?
+ */
+ if (opts.detect_rename > DIFF_DETECT_RENAME)
+ opts.detect_rename = DIFF_DETECT_RENAME;
+ opts.rename_limit = o->merge_rename_limit >= 0 ? o->merge_rename_limit :
+ o->diff_rename_limit >= 0 ? o->diff_rename_limit :
+ 1000;
+ opts.rename_score = o->rename_score;
+ opts.show_rename_progress = o->show_rename_progress;
+ opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+ diff_setup_done(&opts);
+ diff_tree_oid(&o_tree->object.oid, &tree->object.oid, "", &opts);
+ diffcore_std(&opts);
+ if (opts.needed_rename_limit > o->needed_rename_limit)
+ o->needed_rename_limit = opts.needed_rename_limit;
+
+ ret = xmalloc(sizeof(*ret));
+ *ret = diff_queued_diff;
+
+ opts.output_format = DIFF_FORMAT_NO_OUTPUT;
+ diff_queued_diff.nr = 0;
+ diff_queued_diff.queue = NULL;
+ diff_flush(&opts);
+ return ret;
+}
+
+static int tree_has_path(struct tree *tree, const char *path)
+{
+ struct object_id hashy;
+ unsigned int mode_o;
+
+ return !get_tree_entry(&tree->object.oid, path,
+ &hashy, &mode_o);
+}
+
+/*
+ * Return a new string that replaces the beginning portion (which matches
+ * entry->dir), with entry->new_dir. In perl-speak:
+ * new_path_name = (old_path =~ s/entry->dir/entry->new_dir/);
+ * NOTE:
+ * Caller must ensure that old_path starts with entry->dir + '/'.
+ */
+static char *apply_dir_rename(struct dir_rename_entry *entry,
+ const char *old_path)
+{
+ struct strbuf new_path = STRBUF_INIT;
+ int oldlen, newlen;
+
+ if (entry->non_unique_new_dir)
+ return NULL;
+
+ oldlen = strlen(entry->dir);
+ newlen = entry->new_dir.len + (strlen(old_path) - oldlen) + 1;
+ strbuf_grow(&new_path, newlen);
+ strbuf_addbuf(&new_path, &entry->new_dir);
+ strbuf_addstr(&new_path, &old_path[oldlen]);
+
+ return strbuf_detach(&new_path, NULL);
+}
+
+static void get_renamed_dir_portion(const char *old_path, const char *new_path,
+ char **old_dir, char **new_dir)
+{
+ char *end_of_old, *end_of_new;
+ int old_len, new_len;
+
+ *old_dir = NULL;
+ *new_dir = NULL;
+
+ /*
+ * For
+ * "a/b/c/d/e/foo.c" -> "a/b/some/thing/else/e/foo.c"
+ * the "e/foo.c" part is the same, we just want to know that
+ * "a/b/c/d" was renamed to "a/b/some/thing/else"
+ * so, for this example, this function returns "a/b/c/d" in
+ * *old_dir and "a/b/some/thing/else" in *new_dir.
+ *
+ * Also, if the basename of the file changed, we don't care. We
+ * want to know which portion of the directory, if any, changed.
+ */
+ end_of_old = strrchr(old_path, '/');
+ end_of_new = strrchr(new_path, '/');
+
+ if (end_of_old == NULL || end_of_new == NULL)
+ return;
+ while (*--end_of_new == *--end_of_old &&
+ end_of_old != old_path &&
+ end_of_new != new_path)
+ ; /* Do nothing; all in the while loop */
+ /*
+ * We've found the first non-matching character in the directory
+ * paths. That means the current directory we were comparing
+ * represents the rename. Move end_of_old and end_of_new back
+ * to the full directory name.
+ */
+ if (*end_of_old == '/')
+ end_of_old++;
+ if (*end_of_old != '/')
+ end_of_new++;
+ end_of_old = strchr(end_of_old, '/');
+ end_of_new = strchr(end_of_new, '/');
+
+ /*
+ * It may have been the case that old_path and new_path were the same
+ * directory all along. Don't claim a rename if they're the same.
+ */
+ old_len = end_of_old - old_path;
+ new_len = end_of_new - new_path;
+
+ if (old_len != new_len || strncmp(old_path, new_path, old_len)) {
+ *old_dir = xstrndup(old_path, old_len);
+ *new_dir = xstrndup(new_path, new_len);
+ }
+}
+
+static void remove_hashmap_entries(struct hashmap *dir_renames,
+ struct string_list *items_to_remove)
+{
+ int i;
+ struct dir_rename_entry *entry;
+
+ for (i = 0; i < items_to_remove->nr; i++) {
+ entry = items_to_remove->items[i].util;
+ hashmap_remove(dir_renames, entry, NULL);
+ }
+ string_list_clear(items_to_remove, 0);
+}
+
+/*
+ * See if there is a directory rename for path, and if there are any file
+ * level conflicts for the renamed location. If there is a rename and
+ * there are no conflicts, return the new name. Otherwise, return NULL.
+ */
+static char *handle_path_level_conflicts(struct merge_options *o,
+ const char *path,
+ struct dir_rename_entry *entry,
+ struct hashmap *collisions,
+ struct tree *tree)
+{
+ char *new_path = NULL;
+ struct collision_entry *collision_ent;
+ int clean = 1;
+ struct strbuf collision_paths = STRBUF_INIT;
+
+ /*
+ * entry has the mapping of old directory name to new directory name
+ * that we want to apply to path.
+ */
+ new_path = apply_dir_rename(entry, path);
+
+ if (!new_path) {
+ /* This should only happen when entry->non_unique_new_dir set */
+ if (!entry->non_unique_new_dir)
+ BUG("entry->non_unqiue_dir not set and !new_path");
+ output(o, 1, _("CONFLICT (directory rename split): "
+ "Unclear where to place %s because directory "
+ "%s was renamed to multiple other directories, "
+ "with no destination getting a majority of the "
+ "files."),
+ path, entry->dir);
+ clean = 0;
+ return NULL;
+ }
+
+ /*
+ * The caller needs to have ensured that it has pre-populated
+ * collisions with all paths that map to new_path. Do a quick check
+ * to ensure that's the case.
+ */
+ collision_ent = collision_find_entry(collisions, new_path);
+ if (collision_ent == NULL)
+ BUG("collision_ent is NULL");
+
+ /*
+ * Check for one-sided add/add/.../add conflicts, i.e.
+ * where implicit renames from the other side doing
+ * directory rename(s) can affect this side of history
+ * to put multiple paths into the same location. Warn
+ * and bail on directory renames for such paths.
+ */
+ if (collision_ent->reported_already) {
+ clean = 0;
+ } else if (tree_has_path(tree, new_path)) {
+ collision_ent->reported_already = 1;
+ strbuf_add_separated_string_list(&collision_paths, ", ",
+ &collision_ent->source_files);
+ output(o, 1, _("CONFLICT (implicit dir rename): Existing "
+ "file/dir at %s in the way of implicit "
+ "directory rename(s) putting the following "
+ "path(s) there: %s."),
+ new_path, collision_paths.buf);
+ clean = 0;
+ } else if (collision_ent->source_files.nr > 1) {
+ collision_ent->reported_already = 1;
+ strbuf_add_separated_string_list(&collision_paths, ", ",
+ &collision_ent->source_files);
+ output(o, 1, _("CONFLICT (implicit dir rename): Cannot map "
+ "more than one path to %s; implicit directory "
+ "renames tried to put these paths there: %s"),
+ new_path, collision_paths.buf);
+ clean = 0;
+ }
+
+ /* Free memory we no longer need */
+ strbuf_release(&collision_paths);
+ if (!clean && new_path) {
+ free(new_path);
+ return NULL;
+ }
+
+ return new_path;
+}
+
+/*
+ * There are a couple things we want to do at the directory level:
+ * 1. Check for both sides renaming to the same thing, in order to avoid
+ * implicit renaming of files that should be left in place. (See
+ * testcase 6b in t6043 for details.)
+ * 2. Prune directory renames if there are still files left in the
+ * the original directory. These represent a partial directory rename,
+ * i.e. a rename where only some of the files within the directory
+ * were renamed elsewhere. (Technically, this could be done earlier
+ * in get_directory_renames(), except that would prevent us from
+ * doing the previous check and thus failing testcase 6b.)
+ * 3. Check for rename/rename(1to2) conflicts (at the directory level).
+ * In the future, we could potentially record this info as well and
+ * omit reporting rename/rename(1to2) conflicts for each path within
+ * the affected directories, thus cleaning up the merge output.
+ * NOTE: We do NOT check for rename/rename(2to1) conflicts at the
+ * directory level, because merging directories is fine. If it
+ * causes conflicts for files within those merged directories, then
+ * that should be detected at the individual path level.
+ */
+static void handle_directory_level_conflicts(struct merge_options *o,
+ struct hashmap *dir_re_head,
+ struct tree *head,
+ struct hashmap *dir_re_merge,
+ struct tree *merge)
+{
+ struct hashmap_iter iter;
+ struct dir_rename_entry *head_ent;
+ struct dir_rename_entry *merge_ent;
+
+ struct string_list remove_from_head = STRING_LIST_INIT_NODUP;
+ struct string_list remove_from_merge = STRING_LIST_INIT_NODUP;
+
+ hashmap_iter_init(dir_re_head, &iter);
+ while ((head_ent = hashmap_iter_next(&iter))) {
+ merge_ent = dir_rename_find_entry(dir_re_merge, head_ent->dir);
+ if (merge_ent &&
+ !head_ent->non_unique_new_dir &&
+ !merge_ent->non_unique_new_dir &&
+ !strbuf_cmp(&head_ent->new_dir, &merge_ent->new_dir)) {
+ /* 1. Renamed identically; remove it from both sides */
+ string_list_append(&remove_from_head,
+ head_ent->dir)->util = head_ent;
+ strbuf_release(&head_ent->new_dir);
+ string_list_append(&remove_from_merge,
+ merge_ent->dir)->util = merge_ent;
+ strbuf_release(&merge_ent->new_dir);
+ } else if (tree_has_path(head, head_ent->dir)) {
+ /* 2. This wasn't a directory rename after all */
+ string_list_append(&remove_from_head,
+ head_ent->dir)->util = head_ent;
+ strbuf_release(&head_ent->new_dir);
+ }
+ }
+
+ remove_hashmap_entries(dir_re_head, &remove_from_head);
+ remove_hashmap_entries(dir_re_merge, &remove_from_merge);
+
+ hashmap_iter_init(dir_re_merge, &iter);
+ while ((merge_ent = hashmap_iter_next(&iter))) {
+ head_ent = dir_rename_find_entry(dir_re_head, merge_ent->dir);
+ if (tree_has_path(merge, merge_ent->dir)) {
+ /* 2. This wasn't a directory rename after all */
+ string_list_append(&remove_from_merge,
+ merge_ent->dir)->util = merge_ent;
+ } else if (head_ent &&
+ !head_ent->non_unique_new_dir &&
+ !merge_ent->non_unique_new_dir) {
+ /* 3. rename/rename(1to2) */
+ /*
+ * We can assume it's not rename/rename(1to1) because
+ * that was case (1), already checked above. So we
+ * know that head_ent->new_dir and merge_ent->new_dir
+ * are different strings.
+ */
+ output(o, 1, _("CONFLICT (rename/rename): "
+ "Rename directory %s->%s in %s. "
+ "Rename directory %s->%s in %s"),
+ head_ent->dir, head_ent->new_dir.buf, o->branch1,
+ head_ent->dir, merge_ent->new_dir.buf, o->branch2);
+ string_list_append(&remove_from_head,
+ head_ent->dir)->util = head_ent;
+ strbuf_release(&head_ent->new_dir);
+ string_list_append(&remove_from_merge,
+ merge_ent->dir)->util = merge_ent;
+ strbuf_release(&merge_ent->new_dir);
+ }
+ }
+
+ remove_hashmap_entries(dir_re_head, &remove_from_head);
+ remove_hashmap_entries(dir_re_merge, &remove_from_merge);
+}
+
+static struct hashmap *get_directory_renames(struct diff_queue_struct *pairs,
+ struct tree *tree)
+{
+ struct hashmap *dir_renames;
+ struct hashmap_iter iter;
+ struct dir_rename_entry *entry;
+ int i;
+
+ /*
+ * Typically, we think of a directory rename as all files from a
+ * certain directory being moved to a target directory. However,
+ * what if someone first moved two files from the original
+ * directory in one commit, and then renamed the directory
+ * somewhere else in a later commit? At merge time, we just know
+ * that files from the original directory went to two different
+ * places, and that the bulk of them ended up in the same place.
+ * We want each directory rename to represent where the bulk of the
+ * files from that directory end up; this function exists to find
+ * where the bulk of the files went.
+ *
+ * The first loop below simply iterates through the list of file
+ * renames, finding out how often each directory rename pair
+ * possibility occurs.
+ */
+ dir_renames = xmalloc(sizeof(*dir_renames));
+ dir_rename_init(dir_renames);
+ for (i = 0; i < pairs->nr; ++i) {
+ struct string_list_item *item;
+ int *count;
+ struct diff_filepair *pair = pairs->queue[i];
+ char *old_dir, *new_dir;
+
+ /* File not part of directory rename if it wasn't renamed */
+ if (pair->status != 'R')
+ continue;
+
+ get_renamed_dir_portion(pair->one->path, pair->two->path,
+ &old_dir, &new_dir);
+ if (!old_dir)
+ /* Directory didn't change at all; ignore this one. */
+ continue;
+
+ entry = dir_rename_find_entry(dir_renames, old_dir);
+ if (!entry) {
+ entry = xmalloc(sizeof(*entry));
+ dir_rename_entry_init(entry, old_dir);
+ hashmap_put(dir_renames, entry);
+ } else {
+ free(old_dir);
+ }
+ item = string_list_lookup(&entry->possible_new_dirs, new_dir);
+ if (!item) {
+ item = string_list_insert(&entry->possible_new_dirs,
+ new_dir);
+ item->util = xcalloc(1, sizeof(int));
+ } else {
+ free(new_dir);
+ }
+ count = item->util;
+ *count += 1;
+ }
+
+ /*
+ * For each directory with files moved out of it, we find out which
+ * target directory received the most files so we can declare it to
+ * be the "winning" target location for the directory rename. This
+ * winner gets recorded in new_dir. If there is no winner
+ * (multiple target directories received the same number of files),
+ * we set non_unique_new_dir. Once we've determined the winner (or
+ * that there is no winner), we no longer need possible_new_dirs.
+ */
+ hashmap_iter_init(dir_renames, &iter);
+ while ((entry = hashmap_iter_next(&iter))) {
+ int max = 0;
+ int bad_max = 0;
+ char *best = NULL;
+
+ for (i = 0; i < entry->possible_new_dirs.nr; i++) {
+ int *count = entry->possible_new_dirs.items[i].util;
+
+ if (*count == max)
+ bad_max = max;
+ else if (*count > max) {
+ max = *count;
+ best = entry->possible_new_dirs.items[i].string;
+ }
+ }
+ if (bad_max == max)
+ entry->non_unique_new_dir = 1;
+ else {
+ assert(entry->new_dir.len == 0);
+ strbuf_addstr(&entry->new_dir, best);
+ }
+ /*
+ * The relevant directory sub-portion of the original full
+ * filepaths were xstrndup'ed before inserting into
+ * possible_new_dirs, and instead of manually iterating the
+ * list and free'ing each, just lie and tell
+ * possible_new_dirs that it did the strdup'ing so that it
+ * will free them for us.
+ */
+ entry->possible_new_dirs.strdup_strings = 1;
+ string_list_clear(&entry->possible_new_dirs, 1);
+ }
+
+ return dir_renames;
+}
+
+static struct dir_rename_entry *check_dir_renamed(const char *path,
+ struct hashmap *dir_renames)
+{
+ char temp[PATH_MAX];
+ char *end;
+ struct dir_rename_entry *entry;
+
+ strcpy(temp, path);
+ while ((end = strrchr(temp, '/'))) {
+ *end = '\0';
+ entry = dir_rename_find_entry(dir_renames, temp);
+ if (entry)
+ return entry;
+ }
+ return NULL;
+}
+
+static void compute_collisions(struct hashmap *collisions,
+ struct hashmap *dir_renames,
+ struct diff_queue_struct *pairs)
+{
+ int i;
+
+ /*
+ * Multiple files can be mapped to the same path due to directory
+ * renames done by the other side of history. Since that other
+ * side of history could have merged multiple directories into one,
+ * if our side of history added the same file basename to each of
+ * those directories, then all N of them would get implicitly
+ * renamed by the directory rename detection into the same path,
+ * and we'd get an add/add/.../add conflict, and all those adds
+ * from *this* side of history. This is not representable in the
+ * index, and users aren't going to easily be able to make sense of
+ * it. So we need to provide a good warning about what's
+ * happening, and fall back to no-directory-rename detection
+ * behavior for those paths.
+ *
+ * See testcases 9e and all of section 5 from t6043 for examples.
+ */
+ collision_init(collisions);
+
+ for (i = 0; i < pairs->nr; ++i) {
+ struct dir_rename_entry *dir_rename_ent;
+ struct collision_entry *collision_ent;
+ char *new_path;
+ struct diff_filepair *pair = pairs->queue[i];
+
+ if (pair->status != 'A' && pair->status != 'R')
+ continue;
+ dir_rename_ent = check_dir_renamed(pair->two->path,
+ dir_renames);
+ if (!dir_rename_ent)
+ continue;
+
+ new_path = apply_dir_rename(dir_rename_ent, pair->two->path);
+ if (!new_path)
+ /*
+ * dir_rename_ent->non_unique_new_path is true, which
+ * means there is no directory rename for us to use,
+ * which means it won't cause us any additional
+ * collisions.
+ */
+ continue;
+ collision_ent = collision_find_entry(collisions, new_path);
+ if (!collision_ent) {
+ collision_ent = xcalloc(1,
+ sizeof(struct collision_entry));
+ hashmap_entry_init(collision_ent, strhash(new_path));
+ hashmap_put(collisions, collision_ent);
+ collision_ent->target_file = new_path;
+ } else {
+ free(new_path);
+ }
+ string_list_insert(&collision_ent->source_files,
+ pair->two->path);
+ }
+}
+
+static char *check_for_directory_rename(struct merge_options *o,
+ const char *path,
+ struct tree *tree,
+ struct hashmap *dir_renames,
+ struct hashmap *dir_rename_exclusions,
+ struct hashmap *collisions,
+ int *clean_merge)
+{
+ char *new_path = NULL;
+ struct dir_rename_entry *entry = check_dir_renamed(path, dir_renames);
+ struct dir_rename_entry *oentry = NULL;
+
+ if (!entry)
+ return new_path;
+
+ /*
+ * This next part is a little weird. We do not want to do an
+ * implicit rename into a directory we renamed on our side, because
+ * that will result in a spurious rename/rename(1to2) conflict. An
+ * example:
+ * Base commit: dumbdir/afile, otherdir/bfile
+ * Side 1: smrtdir/afile, otherdir/bfile
+ * Side 2: dumbdir/afile, dumbdir/bfile
+ * Here, while working on Side 1, we could notice that otherdir was
+ * renamed/merged to dumbdir, and change the diff_filepair for
+ * otherdir/bfile into a rename into dumbdir/bfile. However, Side
+ * 2 will notice the rename from dumbdir to smrtdir, and do the
+ * transitive rename to move it from dumbdir/bfile to
+ * smrtdir/bfile. That gives us bfile in dumbdir vs being in
+ * smrtdir, a rename/rename(1to2) conflict. We really just want
+ * the file to end up in smrtdir. And the way to achieve that is
+ * to not let Side1 do the rename to dumbdir, since we know that is
+ * the source of one of our directory renames.
+ *
+ * That's why oentry and dir_rename_exclusions is here.
+ *
+ * As it turns out, this also prevents N-way transient rename
+ * confusion; See testcases 9c and 9d of t6043.
+ */
+ oentry = dir_rename_find_entry(dir_rename_exclusions, entry->new_dir.buf);
+ if (oentry) {
+ output(o, 1, _("WARNING: Avoiding applying %s -> %s rename "
+ "to %s, because %s itself was renamed."),
+ entry->dir, entry->new_dir.buf, path, entry->new_dir.buf);
+ } else {
+ new_path = handle_path_level_conflicts(o, path, entry,
+ collisions, tree);
+ *clean_merge &= (new_path != NULL);
+ }
+
+ return new_path;
+}
+
+static void apply_directory_rename_modifications(struct merge_options *o,
+ struct diff_filepair *pair,
+ char *new_path,
+ struct rename *re,
+ struct tree *tree,
+ struct tree *o_tree,
+ struct tree *a_tree,
+ struct tree *b_tree,
+ struct string_list *entries,
+ int *clean)
+{
+ struct string_list_item *item;
+ int stage = (tree == a_tree ? 2 : 3);
+ int update_wd;
+
+ /*
+ * In all cases where we can do directory rename detection,
+ * unpack_trees() will have read pair->two->path into the
+ * index and the working copy. We need to remove it so that
+ * we can instead place it at new_path. It is guaranteed to
+ * not be untracked (unpack_trees() would have errored out
+ * saying the file would have been overwritten), but it might
+ * be dirty, though.
+ */
+ update_wd = !was_dirty(o, pair->two->path);
+ if (!update_wd)
+ output(o, 1, _("Refusing to lose dirty file at %s"),
+ pair->two->path);
+ remove_file(o, 1, pair->two->path, !update_wd);
+
+ /* Find or create a new re->dst_entry */
+ item = string_list_lookup(entries, new_path);
+ if (item) {
+ /*
+ * Since we're renaming on this side of history, and it's
+ * due to a directory rename on the other side of history
+ * (which we only allow when the directory in question no
+ * longer exists on the other side of history), the
+ * original entry for re->dst_entry is no longer
+ * necessary...
+ */
+ re->dst_entry->processed = 1;
+
+ /*
+ * ...because we'll be using this new one.
+ */
+ re->dst_entry = item->util;
+ } else {
+ /*
+ * re->dst_entry is for the before-dir-rename path, and we
+ * need it to hold information for the after-dir-rename
+ * path. Before creating a new entry, we need to mark the
+ * old one as unnecessary (...unless it is shared by
+ * src_entry, i.e. this didn't use to be a rename, in which
+ * case we can just allow the normal processing to happen
+ * for it).
+ */
+ if (pair->status == 'R')
+ re->dst_entry->processed = 1;
+
+ re->dst_entry = insert_stage_data(new_path,
+ o_tree, a_tree, b_tree,
+ entries);
+ item = string_list_insert(entries, new_path);
+ item->util = re->dst_entry;
+ }
+
+ /*
+ * Update the stage_data with the information about the path we are
+ * moving into place. That slot will be empty and available for us
+ * to write to because of the collision checks in
+ * handle_path_level_conflicts(). In other words,
+ * re->dst_entry->stages[stage].oid will be the null_oid, so it's
+ * open for us to write to.
+ *
+ * It may be tempting to actually update the index at this point as
+ * well, using update_stages_for_stage_data(), but as per the big
+ * "NOTE" in update_stages(), doing so will modify the current
+ * in-memory index which will break calls to would_lose_untracked()
+ * that we need to make. Instead, we need to just make sure that
+ * the various conflict_rename_*() functions update the index
+ * explicitly rather than relying on unpack_trees() to have done it.
+ */
+ get_tree_entry(&tree->object.oid,
+ pair->two->path,
+ &re->dst_entry->stages[stage].oid,
+ &re->dst_entry->stages[stage].mode);
+
+ /* Update pair status */
+ if (pair->status == 'A') {
+ /*
+ * Recording rename information for this add makes it look
+ * like a rename/delete conflict. Make sure we can
+ * correctly handle this as an add that was moved to a new
+ * directory instead of reporting a rename/delete conflict.
+ */
+ re->add_turned_into_rename = 1;
+ }
+ /*
+ * We don't actually look at pair->status again, but it seems
+ * pedagogically correct to adjust it.
+ */
+ pair->status = 'R';
+
+ /*
+ * Finally, record the new location.
+ */
+ pair->two->path = new_path;
+}
+
+/*
+ * Get information of all renames which occurred in 'pairs', making use of
+ * any implicit directory renames inferred from the other side of history.
+ * We need the three trees in the merge ('o_tree', 'a_tree' and 'b_tree')
+ * to be able to associate the correct cache entries with the rename
+ * information; tree is always equal to either a_tree or b_tree.
+ */
+static struct string_list *get_renames(struct merge_options *o,
+ struct diff_queue_struct *pairs,
+ struct hashmap *dir_renames,
+ struct hashmap *dir_rename_exclusions,
+ struct tree *tree,
+ struct tree *o_tree,
+ struct tree *a_tree,
+ struct tree *b_tree,
+ struct string_list *entries,
+ int *clean_merge)
+{
+ int i;
+ struct hashmap collisions;
+ struct hashmap_iter iter;
+ struct collision_entry *e;
+ struct string_list *renames;
+
+ compute_collisions(&collisions, dir_renames, pairs);
+ renames = xcalloc(1, sizeof(struct string_list));
+
+ for (i = 0; i < pairs->nr; ++i) {
+ struct string_list_item *item;
+ struct rename *re;
+ struct diff_filepair *pair = pairs->queue[i];
+ char *new_path; /* non-NULL only with directory renames */
+
+ if (pair->status != 'A' && pair->status != 'R') {
+ diff_free_filepair(pair);
+ continue;
+ }
+ new_path = check_for_directory_rename(o, pair->two->path, tree,
+ dir_renames,
+ dir_rename_exclusions,
+ &collisions,
+ clean_merge);
+ if (pair->status != 'R' && !new_path) {
+ diff_free_filepair(pair);
+ continue;
+ }
+
+ re = xmalloc(sizeof(*re));
+ re->processed = 0;
+ re->add_turned_into_rename = 0;
+ re->pair = pair;
+ item = string_list_lookup(entries, re->pair->one->path);
+ if (!item)
+ re->src_entry = insert_stage_data(re->pair->one->path,
+ o_tree, a_tree, b_tree, entries);
+ else
+ re->src_entry = item->util;
+
+ item = string_list_lookup(entries, re->pair->two->path);
+ if (!item)
+ re->dst_entry = insert_stage_data(re->pair->two->path,
+ o_tree, a_tree, b_tree, entries);
+ else
+ re->dst_entry = item->util;
+ item = string_list_insert(renames, pair->one->path);
+ item->util = re;
+ if (new_path)
+ apply_directory_rename_modifications(o, pair, new_path,
+ re, tree, o_tree,
+ a_tree, b_tree,
+ entries,
+ clean_merge);
+ }
+
+ hashmap_iter_init(&collisions, &iter);
+ while ((e = hashmap_iter_next(&iter))) {
+ free(e->target_file);
+ string_list_clear(&e->source_files, 0);
+ }
+ hashmap_free(&collisions, 1);
+ return renames;
+}
+
static int process_renames(struct merge_options *o,
struct string_list *a_renames,
struct string_list *b_renames)
const char *ren2_dst = ren2->pair->two->path;
enum rename_type rename_type;
if (strcmp(ren1_src, ren2_src) != 0)
- die("BUG: ren1_src != ren2_src");
+ BUG("ren1_src != ren2_src");
ren2->dst_entry->processed = 1;
ren2->processed = 1;
if (strcmp(ren1_dst, ren2_dst) != 0) {
ren2 = lookup->util;
ren2_dst = ren2->pair->two->path;
if (strcmp(ren1_dst, ren2_dst) != 0)
- die("BUG: ren1_dst != ren2_dst");
+ BUG("ren1_dst != ren2_dst");
clean_merge = 0;
ren2->processed = 1;
* add-source case).
*/
remove_file(o, 1, ren1_src,
- renamed_stage == 2 || !was_tracked(ren1_src));
+ renamed_stage == 2 || !was_tracked(o, ren1_src));
oidcpy(&src_other.oid,
&ren1->src_entry->stages[other_stage].oid);
dst_other.mode = ren1->dst_entry->stages[other_stage].mode;
try_merge = 0;
- if (oid_eq(&src_other.oid, &null_oid)) {
+ if (oid_eq(&src_other.oid, &null_oid) &&
+ ren1->add_turned_into_rename) {
+ setup_rename_conflict_info(RENAME_DIR,
+ ren1->pair,
+ NULL,
+ branch1,
+ branch2,
+ ren1->dst_entry,
+ NULL,
+ o,
+ NULL,
+ NULL);
+ } else if (oid_eq(&src_other.oid, &null_oid)) {
setup_rename_conflict_info(RENAME_DELETE,
ren1->pair,
NULL,
return clean_merge;
}
+struct rename_info {
+ struct string_list *head_renames;
+ struct string_list *merge_renames;
+};
+
+static void initial_cleanup_rename(struct diff_queue_struct *pairs,
+ struct hashmap *dir_renames)
+{
+ struct hashmap_iter iter;
+ struct dir_rename_entry *e;
+
+ hashmap_iter_init(dir_renames, &iter);
+ while ((e = hashmap_iter_next(&iter))) {
+ free(e->dir);
+ strbuf_release(&e->new_dir);
+ /* possible_new_dirs already cleared in get_directory_renames */
+ }
+ hashmap_free(dir_renames, 1);
+ free(dir_renames);
+
+ free(pairs->queue);
+ free(pairs);
+}
+
+static int handle_renames(struct merge_options *o,
+ struct tree *common,
+ struct tree *head,
+ struct tree *merge,
+ struct string_list *entries,
+ struct rename_info *ri)
+{
+ struct diff_queue_struct *head_pairs, *merge_pairs;
+ struct hashmap *dir_re_head, *dir_re_merge;
+ int clean = 1;
+
+ ri->head_renames = NULL;
+ ri->merge_renames = NULL;
+
+ if (!merge_detect_rename(o))
+ return 1;
+
+ head_pairs = get_diffpairs(o, common, head);
+ merge_pairs = get_diffpairs(o, common, merge);
+
+ dir_re_head = get_directory_renames(head_pairs, head);
+ dir_re_merge = get_directory_renames(merge_pairs, merge);
+
+ handle_directory_level_conflicts(o,
+ dir_re_head, head,
+ dir_re_merge, merge);
+
+ ri->head_renames = get_renames(o, head_pairs,
+ dir_re_merge, dir_re_head, head,
+ common, head, merge, entries,
+ &clean);
+ if (clean < 0)
+ goto cleanup;
+ ri->merge_renames = get_renames(o, merge_pairs,
+ dir_re_head, dir_re_merge, merge,
+ common, head, merge, entries,
+ &clean);
+ if (clean < 0)
+ goto cleanup;
+ clean &= process_renames(o, ri->head_renames, ri->merge_renames);
+
+cleanup:
+ /*
+ * Some cleanup is deferred until cleanup_renames() because the
+ * data structures are still needed and referenced in
+ * process_entry(). But there are a few things we can free now.
+ */
+ initial_cleanup_rename(head_pairs, dir_re_head);
+ initial_cleanup_rename(merge_pairs, dir_re_merge);
+
+ return clean;
+}
+
+static void final_cleanup_rename(struct string_list *rename)
+{
+ const struct rename *re;
+ int i;
+
+ if (rename == NULL)
+ return;
+
+ for (i = 0; i < rename->nr; i++) {
+ re = rename->items[i].util;
+ diff_free_filepair(re->pair);
+ }
+ string_list_clear(rename, 1);
+ free(rename);
+}
+
+static void final_cleanup_renames(struct rename_info *re_info)
+{
+ final_cleanup_rename(re_info->head_renames);
+ final_cleanup_rename(re_info->merge_renames);
+}
+
static struct object_id *stage_oid(const struct object_id *oid, unsigned mode)
{
return (is_null_oid(oid) || mode == 0) ? NULL: (struct object_id *)oid;
static int merge_content(struct merge_options *o,
const char *path,
+ int is_dirty,
struct object_id *o_oid, int o_mode,
struct object_id *a_oid, int a_mode,
struct object_id *b_oid, int b_mode,
S_ISGITLINK(pair1->two->mode)))
df_conflict_remains = 1;
}
- if (merge_file_special_markers(o, &one, &a, &b,
+ if (merge_file_special_markers(o, &one, &a, &b, path,
o->branch1, path1,
o->branch2, path2, &mfi))
return -1;
- if (mfi.clean && !df_conflict_remains &&
- oid_eq(&mfi.oid, a_oid) && mfi.mode == a_mode) {
- int path_renamed_outside_HEAD;
+ /*
+ * We can skip updating the working tree file iff:
+ * a) The merge is clean
+ * b) The merge matches what was in HEAD (content, mode, pathname)
+ * c) The target path is usable (i.e. not involved in D/F conflict)
+ */
+ if (mfi.clean &&
+ was_tracked_and_matches(o, path, &mfi.oid, mfi.mode) &&
+ !df_conflict_remains) {
output(o, 3, _("Skipped %s (merged same as existing)"), path);
- /*
- * The content merge resulted in the same file contents we
- * already had. We can return early if those file contents
- * are recorded at the correct path (which may not be true
- * if the merge involves a rename).
- */
- path_renamed_outside_HEAD = !path2 || !strcmp(path, path2);
- if (!path_renamed_outside_HEAD) {
- add_cacheinfo(o, mfi.mode, &mfi.oid, path,
- 0, (!o->call_depth), 0);
- return mfi.clean;
- }
- } else
- output(o, 2, _("Auto-merging %s"), path);
+ if (add_cacheinfo(o, mfi.mode, &mfi.oid, path,
+ 0, (!o->call_depth && !is_dirty), 0))
+ return -1;
+ return mfi.clean;
+ }
if (!mfi.clean) {
if (S_ISGITLINK(mfi.mode))
return -1;
}
- if (df_conflict_remains) {
+ if (df_conflict_remains || is_dirty) {
char *new_path;
if (o->call_depth) {
remove_file_from_cache(path);
if (update_stages(o, path, &one, &a, &b))
return -1;
} else {
- int file_from_stage2 = was_tracked(path);
+ int file_from_stage2 = was_tracked(o, path);
struct diff_filespec merged;
oidcpy(&merged.oid, &mfi.oid);
merged.mode = mfi.mode;
}
new_path = unique_path(o, path, rename_conflict_info->branch1);
+ if (is_dirty) {
+ output(o, 1, _("Refusing to lose dirty file at %s"),
+ path);
+ }
output(o, 1, _("Adding as %s instead"), new_path);
if (update_file(o, 0, &mfi.oid, mfi.mode, new_path)) {
free(new_path);
mfi.clean = 0;
} else if (update_file(o, mfi.clean, &mfi.oid, mfi.mode, path))
return -1;
- return mfi.clean;
+ return !is_dirty && mfi.clean;
+}
+
+static int conflict_rename_normal(struct merge_options *o,
+ const char *path,
+ struct object_id *o_oid, unsigned int o_mode,
+ struct object_id *a_oid, unsigned int a_mode,
+ struct object_id *b_oid, unsigned int b_mode,
+ struct rename_conflict_info *ci)
+{
+ /* Merge the content and write it out */
+ return merge_content(o, path, was_dirty(o, path),
+ o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,
+ ci);
}
/* Per entry merge function */
switch (conflict_info->rename_type) {
case RENAME_NORMAL:
case RENAME_ONE_FILE_TO_ONE:
- clean_merge = merge_content(o, path,
- o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,
- conflict_info);
+ clean_merge = conflict_rename_normal(o,
+ path,
+ o_oid, o_mode,
+ a_oid, a_mode,
+ b_oid, b_mode,
+ conflict_info);
+ break;
+ case RENAME_DIR:
+ clean_merge = 1;
+ if (conflict_rename_dir(o,
+ conflict_info->pair1,
+ conflict_info->branch1,
+ conflict_info->branch2))
+ clean_merge = -1;
break;
case RENAME_DELETE:
clean_merge = 0;
} else if (a_oid && b_oid) {
/* Case C: Added in both (check for same permissions) and */
/* case D: Modified in both, but differently. */
- clean_merge = merge_content(o, path,
+ int is_dirty = 0; /* unpack_trees would have bailed if dirty */
+ clean_merge = merge_content(o, path, is_dirty,
o_oid, o_mode, a_oid, a_mode, b_oid, b_mode,
NULL);
} else if (!o_oid && !a_oid && !b_oid) {
*/
remove_file(o, 1, path, !a_mode);
} else
- die("BUG: fatal merge failure, shouldn't happen.");
+ BUG("fatal merge failure, shouldn't happen.");
return clean_merge;
}
return 1;
}
- code = git_merge_trees(o->call_depth, common, head, merge);
+ code = git_merge_trees(o, common, head, merge);
if (code != 0) {
if (show(o, 4) || o->call_depth)
}
if (unmerged_cache()) {
- struct string_list *entries, *re_head, *re_merge;
+ struct string_list *entries;
+ struct rename_info re_info;
int i;
/*
* Only need the hashmap while processing entries, so
get_files_dirs(o, merge);
entries = get_unmerged();
- re_head = get_renames(o, head, common, head, merge, entries);
- re_merge = get_renames(o, merge, common, head, merge, entries);
- clean = process_renames(o, re_head, re_merge);
+ clean = handle_renames(o, common, head, merge, entries,
+ &re_info);
record_df_conflict_files(o, entries);
if (clean < 0)
goto cleanup;
for (i = 0; i < entries->nr; i++) {
struct stage_data *e = entries->items[i].util;
if (!e->processed)
- die("BUG: unprocessed path??? %s",
+ BUG("unprocessed path??? %s",
entries->items[i].string);
}
cleanup:
- string_list_clear(re_merge, 0);
- string_list_clear(re_head, 0);
+ final_cleanup_renames(&re_info);
+
string_list_clear(entries, 1);
+ free(entries);
hashmap_free(&o->current_file_dir_set, 1);
- free(re_merge);
- free(re_head);
- free(entries);
-
if (clean < 0)
return clean;
}
else
clean = 1;
+ /* Free the extra index left from git_merge_trees() */
+ /*
+ * FIXME: Need to also free data allocated by
+ * setup_unpack_trees_porcelain() tucked away in o->unpack_opts.msgs,
+ * but the problem is that only half of it refers to dynamically
+ * allocated data, while the other half points at static strings.
+ */
+ discard_index(&o->orig_index);
+
if (o->call_depth && !(*result = write_tree_from_memory(o)))
return -1;
read_cache();
o->ancestor = "merged common ancestors";
- clean = merge_trees(o, h1->tree, h2->tree, merged_common_ancestors->tree,
+ clean = merge_trees(o, get_commit_tree(h1), get_commit_tree(h2),
+ get_commit_tree(merged_common_ancestors),
&mrtree);
if (clean < 0) {
flush_output(o);
static void merge_recursive_config(struct merge_options *o)
{
+ char *value = NULL;
git_config_get_int("merge.verbosity", &o->verbosity);
git_config_get_int("diff.renamelimit", &o->diff_rename_limit);
git_config_get_int("merge.renamelimit", &o->merge_rename_limit);
+ if (!git_config_get_string("diff.renames", &value)) {
+ o->diff_detect_rename = git_config_rename("diff.renames", value);
+ free(value);
+ }
+ if (!git_config_get_string("merge.renames", &value)) {
+ o->merge_detect_rename = git_config_rename("merge.renames", value);
+ free(value);
+ }
git_config(git_xmerge_config, NULL);
}
o->diff_rename_limit = -1;
o->merge_rename_limit = -1;
o->renormalize = 0;
- o->detect_rename = 1;
+ o->diff_detect_rename = -1;
+ o->merge_detect_rename = -1;
merge_recursive_config(o);
merge_verbosity = getenv("GIT_MERGE_VERBOSITY");
if (merge_verbosity)
else if (!strcmp(s, "no-renormalize"))
o->renormalize = 0;
else if (!strcmp(s, "no-renames"))
- o->detect_rename = 0;
+ o->merge_detect_rename = 0;
else if (!strcmp(s, "find-renames")) {
- o->detect_rename = 1;
+ o->merge_detect_rename = 1;
o->rename_score = 0;
}
else if (skip_prefix(s, "find-renames=", &arg) ||
skip_prefix(s, "rename-threshold=", &arg)) {
if ((o->rename_score = parse_rename_score(&arg)) == -1 || *arg != 0)
return -1;
- o->detect_rename = 1;
+ o->merge_detect_rename = 1;
}
else
return -1;
#ifndef MERGE_RECURSIVE_H
#define MERGE_RECURSIVE_H
+#include "unpack-trees.h"
#include "string-list.h"
struct merge_options {
unsigned renormalize : 1;
long xdl_opts;
int verbosity;
- int detect_rename;
+ int diff_detect_rename;
+ int merge_detect_rename;
int diff_rename_limit;
int merge_rename_limit;
int rename_score;
struct strbuf obuf;
struct hashmap current_file_dir_set;
struct string_list df_conflict_file_set;
+ struct unpack_trees_options unpack_opts;
+ struct index_state orig_index;
};
+/*
+ * For dir_rename_entry, directory names are stored as a full path from the
+ * toplevel of the repository and do not include a trailing '/'. Also:
+ *
+ * dir: original name of directory being renamed
+ * non_unique_new_dir: if true, could not determine new_dir
+ * new_dir: final name of directory being renamed
+ * possible_new_dirs: temporary used to help determine new_dir; see comments
+ * in get_directory_renames() for details
+ */
+struct dir_rename_entry {
+ struct hashmap_entry ent; /* must be the first member! */
+ char *dir;
+ unsigned non_unique_new_dir:1;
+ struct strbuf new_dir;
+ struct string_list possible_new_dirs;
+};
+
+struct collision_entry {
+ struct hashmap_entry ent; /* must be the first member! */
+ char *target_file;
+ struct string_list source_files;
+ unsigned reported_already:1;
+};
+
+static inline int merge_detect_rename(struct merge_options *o)
+{
+ return o->merge_detect_rename >= 0 ? o->merge_detect_rename :
+ o->diff_detect_rename >= 0 ? o->diff_detect_rename : 1;
+}
+
/* merge_trees() but with recursive ancestor consolidation */
int merge_recursive(struct merge_options *o,
struct commit *h1,
static const char *merge_argument(struct commit *commit)
{
- if (commit)
- return oid_to_hex(&commit->object.oid);
- else
- return EMPTY_TREE_SHA1_HEX;
+ return oid_to_hex(commit ? &commit->object.oid : the_hash_algo->empty_tree);
}
int index_has_changes(struct strbuf *sb)
printf("Using remote notes for %s\n",
oid_to_hex(&p->obj));
if (add_note(t, &p->obj, &p->remote, combine_notes_overwrite))
- die("BUG: combine_notes_overwrite failed");
+ BUG("combine_notes_overwrite failed");
return 0;
case NOTES_MERGE_RESOLVE_UNION:
if (o->verbosity >= 2)
trace_printf("\t\t\tno local change, adopted remote\n");
if (add_note(t, &p->obj, &p->remote,
combine_notes_overwrite))
- die("BUG: combine_notes_overwrite failed");
+ BUG("combine_notes_overwrite failed");
} else {
/* need file-level merge between local and remote */
trace_printf("\t\t\tneed content-level merge\n");
printf("No merge base found; doing history-less merge\n");
} else if (!bases->next) {
base_oid = &bases->item->object.oid;
- base_tree_oid = &bases->item->tree->object.oid;
+ base_tree_oid = get_commit_tree_oid(bases->item);
if (o->verbosity >= 4)
printf("One merge base found (%.7s)\n",
oid_to_hex(base_oid));
} else {
/* TODO: How to handle multiple merge-bases? */
base_oid = &bases->item->object.oid;
- base_tree_oid = &bases->item->tree->object.oid;
+ base_tree_oid = get_commit_tree_oid(bases->item);
if (o->verbosity >= 3)
printf("Multiple merge bases found. Using the first "
"(%.7s)\n", oid_to_hex(base_oid));
goto found_result;
}
- result = merge_from_diffs(o, base_tree_oid, &local->tree->object.oid,
- &remote->tree->object.oid, local_tree);
+ result = merge_from_diffs(o, base_tree_oid,
+ get_commit_tree_oid(local),
+ get_commit_tree_oid(remote), local_tree);
if (result != 0) { /* non-trivial merge (with or without conflicts) */
/* Commit (partial) result */
int index_version;
time_t mtime;
int pack_fd;
+ int index; /* for builtin/pack-objects.c */
unsigned pack_local:1,
pack_keep:1,
+ pack_keep_in_core:1,
freshened:1,
do_not_close:1,
pack_promisor:1;
if ((obj && obj->type == OBJ_BLOB && has_object_file(oid)) ||
(!obj && has_object_file(oid) &&
- oid_object_info(oid, NULL) == OBJ_BLOB)) {
+ oid_object_info(the_repository, oid, NULL) == OBJ_BLOB)) {
if (check_object_signature(repl, NULL, 0, NULL) < 0) {
error("sha1 mismatch %s", oid_to_hex(oid));
return NULL;
FREE_AND_NULL(o->objectdir);
FREE_AND_NULL(o->alternate_db);
+ oidmap_free(o->replace_map, 1);
+ FREE_AND_NULL(o->replace_map);
+
free_alt_odbs(o);
o->alt_odb_tail = NULL;
#define OBJECT_ARRAY_INIT { 0, 0, NULL }
-#define TYPE_BITS 3
/*
* object flag allocation:
* revision.h: 0---------10 26
/**
* Build the initial type index for the packfile
*/
-void bitmap_writer_build_type_index(struct pack_idx_entry **index,
+void bitmap_writer_build_type_index(struct packing_data *to_pack,
+ struct pack_idx_entry **index,
uint32_t index_nr)
{
uint32_t i;
writer.trees = ewah_new();
writer.blobs = ewah_new();
writer.tags = ewah_new();
+ ALLOC_ARRAY(to_pack->in_pack_pos, to_pack->nr_objects);
for (i = 0; i < index_nr; ++i) {
struct object_entry *entry = (struct object_entry *)index[i];
enum object_type real_type;
- entry->in_pack_pos = i;
+ oe_set_in_pack_pos(to_pack, entry, i);
- switch (entry->type) {
+ switch (oe_type(entry)) {
case OBJ_COMMIT:
case OBJ_TREE:
case OBJ_BLOB:
case OBJ_TAG:
- real_type = entry->type;
+ real_type = oe_type(entry);
break;
default:
- real_type = oid_object_info(&entry->idx.oid, NULL);
+ real_type = oid_object_info(the_repository,
+ &entry->idx.oid, NULL);
break;
}
default:
die("Missing type information for %s (%d/%d)",
oid_to_hex(&entry->idx.oid), real_type,
- entry->type);
+ oe_type(entry));
}
}
}
"(object %s is missing)", sha1_to_hex(sha1));
}
- return entry->in_pack_pos;
+ return oe_in_pack_pos(writer.to_pack, entry);
}
static void show_object(struct object *object, const char *name, void *data)
sha1_pos(stored->commit->object.oid.hash, index, index_nr, sha1_access);
if (commit_pos < 0)
- die("BUG: trying to write commit not in index");
+ BUG("trying to write commit not in index");
hashwrite_be32(f, commit_pos);
hashwrite_u8(f, stored->xor_offset);
size_t len;
if (!strip_suffix(p->pack_name, ".pack", &len))
- die("BUG: pack_name does not end in .pack");
+ BUG("pack_name does not end in .pack");
return xstrfmt("%.*s.bitmap", (int)len, p->pack_name);
}
revs->ignore_missing_links = 0;
if (haves_bitmap == NULL)
- die("BUG: failed to perform bitmap walk");
+ BUG("failed to perform bitmap walk");
}
wants_bitmap = find_objects(revs, wants, haves_bitmap);
if (!wants_bitmap)
- die("BUG: failed to perform bitmap walk");
+ BUG("failed to perform bitmap walk");
if (haves_bitmap)
bitmap_and_not(wants_bitmap, haves_bitmap);
oe = packlist_find(mapping, sha1, NULL);
if (oe)
- reposition[i] = oe->in_pack_pos + 1;
+ reposition[i] = oe_in_pack_pos(mapping, oe) + 1;
}
rebuild = bitmap_new();
void bitmap_writer_show_progress(int show);
void bitmap_writer_set_checksum(unsigned char *sha1);
-void bitmap_writer_build_type_index(struct pack_idx_entry **index, uint32_t index_nr);
+void bitmap_writer_build_type_index(struct packing_data *to_pack,
+ struct pack_idx_entry **index,
+ uint32_t index_nr);
void bitmap_writer_reuse_bitmaps(struct packing_data *to_pack);
void bitmap_writer_select_commits(struct commit **indexed_commits,
unsigned int indexed_commits_nr, int max_bitmaps);
#include "cache.h"
+#include "repository.h"
#include "pack.h"
#include "pack-revindex.h"
#include "progress.h"
data = NULL;
data_valid = 0;
} else {
- data = unpack_entry(p, entries[i].offset, &type, &size);
+ data = unpack_entry(the_repository, p, entries[i].offset, &type, &size);
data_valid = 1;
}
#include "object.h"
#include "pack.h"
#include "pack-objects.h"
+#include "packfile.h"
+#include "config.h"
static uint32_t locate_object_entry_hash(struct packing_data *pdata,
const unsigned char *sha1,
&found);
if (found)
- die("BUG: Duplicate object in hash");
+ BUG("Duplicate object in hash");
pdata->index[ix] = i + 1;
entry++;
return &pdata->objects[pdata->index[i] - 1];
}
+static void prepare_in_pack_by_idx(struct packing_data *pdata)
+{
+ struct packed_git **mapping, *p;
+ int cnt = 0, nr = 1U << OE_IN_PACK_BITS;
+
+ ALLOC_ARRAY(mapping, nr);
+ /*
+ * oe_in_pack() on an all-zero'd object_entry
+ * (i.e. in_pack_idx also zero) should return NULL.
+ */
+ mapping[cnt++] = NULL;
+ for (p = get_packed_git(the_repository); p; p = p->next, cnt++) {
+ if (cnt == nr) {
+ free(mapping);
+ return;
+ }
+ p->index = cnt;
+ mapping[cnt] = p;
+ }
+ pdata->in_pack_by_idx = mapping;
+}
+
+/*
+ * A new pack appears after prepare_in_pack_by_idx() has been
+ * run. This is likely a race.
+ *
+ * We could map this new pack to in_pack_by_idx[] array, but then we
+ * have to deal with full array anyway. And since it's hard to test
+ * this fall back code, just stay simple and fall back to using
+ * in_pack[] array.
+ */
+void oe_map_new_pack(struct packing_data *pack,
+ struct packed_git *p)
+{
+ uint32_t i;
+
+ REALLOC_ARRAY(pack->in_pack, pack->nr_alloc);
+
+ for (i = 0; i < pack->nr_objects; i++)
+ pack->in_pack[i] = oe_in_pack(pack, pack->objects + i);
+
+ FREE_AND_NULL(pack->in_pack_by_idx);
+}
+
+/* assume pdata is already zero'd by caller */
+void prepare_packing_data(struct packing_data *pdata)
+{
+ if (git_env_bool("GIT_TEST_FULL_IN_PACK_ARRAY", 0)) {
+ /*
+ * do not initialize in_pack_by_idx[] to force the
+ * slow path in oe_in_pack()
+ */
+ } else {
+ prepare_in_pack_by_idx(pdata);
+ }
+
+ pdata->oe_size_limit = git_env_ulong("GIT_TEST_OE_SIZE",
+ 1U << OE_SIZE_BITS);
+}
+
struct object_entry *packlist_alloc(struct packing_data *pdata,
const unsigned char *sha1,
uint32_t index_pos)
if (pdata->nr_objects >= pdata->nr_alloc) {
pdata->nr_alloc = (pdata->nr_alloc + 1024) * 3 / 2;
REALLOC_ARRAY(pdata->objects, pdata->nr_alloc);
+
+ if (!pdata->in_pack_by_idx)
+ REALLOC_ARRAY(pdata->in_pack, pdata->nr_alloc);
}
new_entry = pdata->objects + pdata->nr_objects++;
else
pdata->index[index_pos] = pdata->nr_objects;
+ if (pdata->in_pack)
+ pdata->in_pack[pdata->nr_objects - 1] = NULL;
+
return new_entry;
}
#ifndef PACK_OBJECTS_H
#define PACK_OBJECTS_H
+#include "object-store.h"
+
+#define DEFAULT_DELTA_CACHE_SIZE (256 * 1024 * 1024)
+
+#define OE_DFS_STATE_BITS 2
+#define OE_DEPTH_BITS 12
+#define OE_IN_PACK_BITS 10
+#define OE_Z_DELTA_BITS 20
+/*
+ * Note that oe_set_size() becomes expensive when the given size is
+ * above this limit. Don't lower it too much.
+ */
+#define OE_SIZE_BITS 31
+#define OE_DELTA_SIZE_BITS 20
+
+/*
+ * State flags for depth-first search used for analyzing delta cycles.
+ *
+ * The depth is measured in delta-links to the base (so if A is a delta
+ * against B, then A has a depth of 1, and B a depth of 0).
+ */
+enum dfs_state {
+ DFS_NONE = 0,
+ DFS_ACTIVE,
+ DFS_DONE,
+ DFS_NUM_STATES
+};
+
+/*
+ * The size of struct nearly determines pack-objects's memory
+ * consumption. This struct is packed tight for that reason. When you
+ * add or reorder something in this struct, think a bit about this.
+ *
+ * basic object info
+ * -----------------
+ * idx.oid is filled up before delta searching starts. idx.crc32 is
+ * only valid after the object is written out and will be used for
+ * generating the index. idx.offset will be both gradually set and
+ * used in writing phase (base objects get offset first, then deltas
+ * refer to them)
+ *
+ * "size" is the uncompressed object size. Compressed size of the raw
+ * data for an object in a pack is not stored anywhere but is computed
+ * and made available when reverse .idx is made. Note that when a
+ * delta is reused, "size" is the uncompressed _delta_ size, not the
+ * canonical one after the delta has been applied.
+ *
+ * "hash" contains a path name hash which is used for sorting the
+ * delta list and also during delta searching. Once prepare_pack()
+ * returns it's no longer needed.
+ *
+ * source pack info
+ * ----------------
+ * The (in_pack, in_pack_offset) tuple contains the location of the
+ * object in the source pack. in_pack_header_size allows quickly
+ * skipping the header and going straight to the zlib stream.
+ *
+ * "type" and "in_pack_type" both describe object type. in_pack_type
+ * may contain a delta type, while type is always the canonical type.
+ *
+ * deltas
+ * ------
+ * Delta links (delta, delta_child and delta_sibling) are created to
+ * reflect that delta graph from the source pack then updated or added
+ * during delta searching phase when we find better deltas.
+ *
+ * delta_child and delta_sibling are last needed in
+ * compute_write_order(). "delta" and "delta_size" must remain valid
+ * at object writing phase in case the delta is not cached.
+ *
+ * If a delta is cached in memory and is compressed, delta_data points
+ * to the data and z_delta_size contains the compressed size. If it's
+ * uncompressed [1], z_delta_size must be zero. delta_size is always
+ * the uncompressed size and must be valid even if the delta is not
+ * cached.
+ *
+ * [1] during try_delta phase we don't bother with compressing because
+ * the delta could be quickly replaced with a better one.
+ */
struct object_entry {
struct pack_idx_entry idx;
- unsigned long size; /* uncompressed size */
- struct packed_git *in_pack; /* already in pack */
- off_t in_pack_offset;
- struct object_entry *delta; /* delta base object */
- struct object_entry *delta_child; /* deltified objects who bases me */
- struct object_entry *delta_sibling; /* other deltified objects who
- * uses the same base as me
- */
void *delta_data; /* cached delta (uncompressed) */
- unsigned long delta_size; /* delta data size (uncompressed) */
- unsigned long z_delta_size; /* delta data size (compressed) */
- enum object_type type;
- enum object_type in_pack_type; /* could be delta */
+ off_t in_pack_offset;
uint32_t hash; /* name hint hash */
- unsigned int in_pack_pos;
- unsigned char in_pack_header_size;
+ unsigned size_:OE_SIZE_BITS;
+ unsigned size_valid:1;
+ uint32_t delta_idx; /* delta base object */
+ uint32_t delta_child_idx; /* deltified objects who bases me */
+ uint32_t delta_sibling_idx; /* other deltified objects who
+ * uses the same base as me
+ */
+ unsigned delta_size_:OE_DELTA_SIZE_BITS; /* delta data size (uncompressed) */
+ unsigned delta_size_valid:1;
+ unsigned in_pack_idx:OE_IN_PACK_BITS; /* already in pack */
+ unsigned z_delta_size:OE_Z_DELTA_BITS;
+ unsigned type_valid:1;
+ unsigned type_:TYPE_BITS;
+ unsigned no_try_delta:1;
+ unsigned in_pack_type:TYPE_BITS; /* could be delta */
unsigned preferred_base:1; /*
* we do not pack this, but is available
* to be used as the base object to delta
* objects against.
*/
- unsigned no_try_delta:1;
unsigned tagged:1; /* near the very tip of refs */
unsigned filled:1; /* assigned write-order */
+ unsigned dfs_state:OE_DFS_STATE_BITS;
+ unsigned char in_pack_header_size;
+ unsigned depth:OE_DEPTH_BITS;
/*
- * State flags for depth-first search used for analyzing delta cycles.
+ * pahole results on 64-bit linux (gcc and clang)
+ *
+ * size: 80, bit_padding: 20 bits, holes: 8 bits
+ *
+ * and on 32-bit (gcc)
*
- * The depth is measured in delta-links to the base (so if A is a delta
- * against B, then A has a depth of 1, and B a depth of 0).
+ * size: 76, bit_padding: 20 bits, holes: 8 bits
*/
- enum {
- DFS_NONE = 0,
- DFS_ACTIVE,
- DFS_DONE
- } dfs_state;
- int depth;
};
struct packing_data {
int32_t *index;
uint32_t index_size;
+
+ unsigned int *in_pack_pos;
+
+ /*
+ * Only one of these can be non-NULL and they have different
+ * sizes. if in_pack_by_idx is allocated, oe_in_pack() returns
+ * the pack of an object using in_pack_idx field. If not,
+ * in_pack[] array is used the same way as in_pack_pos[]
+ */
+ struct packed_git **in_pack_by_idx;
+ struct packed_git **in_pack;
+
+ uintmax_t oe_size_limit;
};
+void prepare_packing_data(struct packing_data *pdata);
struct object_entry *packlist_alloc(struct packing_data *pdata,
const unsigned char *sha1,
uint32_t index_pos);
return hash;
}
+static inline enum object_type oe_type(const struct object_entry *e)
+{
+ return e->type_valid ? e->type_ : OBJ_BAD;
+}
+
+static inline void oe_set_type(struct object_entry *e,
+ enum object_type type)
+{
+ if (type >= OBJ_ANY)
+ BUG("OBJ_ANY cannot be set in pack-objects code");
+
+ e->type_valid = type >= OBJ_NONE;
+ e->type_ = (unsigned)type;
+}
+
+static inline unsigned int oe_in_pack_pos(const struct packing_data *pack,
+ const struct object_entry *e)
+{
+ return pack->in_pack_pos[e - pack->objects];
+}
+
+static inline void oe_set_in_pack_pos(const struct packing_data *pack,
+ const struct object_entry *e,
+ unsigned int pos)
+{
+ pack->in_pack_pos[e - pack->objects] = pos;
+}
+
+static inline struct packed_git *oe_in_pack(const struct packing_data *pack,
+ const struct object_entry *e)
+{
+ if (pack->in_pack_by_idx)
+ return pack->in_pack_by_idx[e->in_pack_idx];
+ else
+ return pack->in_pack[e - pack->objects];
+}
+
+void oe_map_new_pack(struct packing_data *pack,
+ struct packed_git *p);
+static inline void oe_set_in_pack(struct packing_data *pack,
+ struct object_entry *e,
+ struct packed_git *p)
+{
+ if (!p->index)
+ oe_map_new_pack(pack, p);
+ if (pack->in_pack_by_idx)
+ e->in_pack_idx = p->index;
+ else
+ pack->in_pack[e - pack->objects] = p;
+}
+
+static inline struct object_entry *oe_delta(
+ const struct packing_data *pack,
+ const struct object_entry *e)
+{
+ if (e->delta_idx)
+ return &pack->objects[e->delta_idx - 1];
+ return NULL;
+}
+
+static inline void oe_set_delta(struct packing_data *pack,
+ struct object_entry *e,
+ struct object_entry *delta)
+{
+ if (delta)
+ e->delta_idx = (delta - pack->objects) + 1;
+ else
+ e->delta_idx = 0;
+}
+
+static inline struct object_entry *oe_delta_child(
+ const struct packing_data *pack,
+ const struct object_entry *e)
+{
+ if (e->delta_child_idx)
+ return &pack->objects[e->delta_child_idx - 1];
+ return NULL;
+}
+
+static inline void oe_set_delta_child(struct packing_data *pack,
+ struct object_entry *e,
+ struct object_entry *delta)
+{
+ if (delta)
+ e->delta_child_idx = (delta - pack->objects) + 1;
+ else
+ e->delta_child_idx = 0;
+}
+
+static inline struct object_entry *oe_delta_sibling(
+ const struct packing_data *pack,
+ const struct object_entry *e)
+{
+ if (e->delta_sibling_idx)
+ return &pack->objects[e->delta_sibling_idx - 1];
+ return NULL;
+}
+
+static inline void oe_set_delta_sibling(struct packing_data *pack,
+ struct object_entry *e,
+ struct object_entry *delta)
+{
+ if (delta)
+ e->delta_sibling_idx = (delta - pack->objects) + 1;
+ else
+ e->delta_sibling_idx = 0;
+}
+
+unsigned long oe_get_size_slow(struct packing_data *pack,
+ const struct object_entry *e);
+static inline unsigned long oe_size(struct packing_data *pack,
+ const struct object_entry *e)
+{
+ if (e->size_valid)
+ return e->size_;
+
+ return oe_get_size_slow(pack, e);
+}
+
+static inline int oe_size_less_than(struct packing_data *pack,
+ const struct object_entry *lhs,
+ unsigned long rhs)
+{
+ if (lhs->size_valid)
+ return lhs->size_ < rhs;
+ if (rhs < pack->oe_size_limit) /* rhs < 2^x <= lhs ? */
+ return 0;
+ return oe_get_size_slow(pack, lhs) < rhs;
+}
+
+static inline int oe_size_greater_than(struct packing_data *pack,
+ const struct object_entry *lhs,
+ unsigned long rhs)
+{
+ if (lhs->size_valid)
+ return lhs->size_ > rhs;
+ if (rhs < pack->oe_size_limit) /* rhs < 2^x <= lhs ? */
+ return 1;
+ return oe_get_size_slow(pack, lhs) > rhs;
+}
+
+static inline void oe_set_size(struct packing_data *pack,
+ struct object_entry *e,
+ unsigned long size)
+{
+ if (size < pack->oe_size_limit) {
+ e->size_ = size;
+ e->size_valid = 1;
+ } else {
+ e->size_valid = 0;
+ if (oe_get_size_slow(pack, e) != size)
+ BUG("'size' is supposed to be the object size!");
+ }
+}
+
+static inline unsigned long oe_delta_size(struct packing_data *pack,
+ const struct object_entry *e)
+{
+ if (e->delta_size_valid)
+ return e->delta_size_;
+ return oe_size(pack, e);
+}
+
+static inline void oe_set_delta_size(struct packing_data *pack,
+ struct object_entry *e,
+ unsigned long size)
+{
+ e->delta_size_ = size;
+ e->delta_size_valid = e->delta_size_ == size;
+ if (!e->delta_size_valid && size != oe_size(pack, e))
+ BUG("this can only happen in check_object() "
+ "where delta size is the same as entry size");
+}
+
#endif
uint32_t version, nr, i, *index;
int fd = git_open(path);
struct stat st;
+ const unsigned int hashsz = the_hash_algo->rawsz;
if (fd < 0)
return -1;
return -1;
}
idx_size = xsize_t(st.st_size);
- if (idx_size < 4 * 256 + 20 + 20) {
+ if (idx_size < 4 * 256 + hashsz + hashsz) {
close(fd);
return error("index file %s is too small", path);
}
/*
* Total size:
* - 256 index entries 4 bytes each
- * - 24-byte entries * nr (20-byte sha1 + 4-byte offset)
- * - 20-byte SHA1 of the packfile
- * - 20-byte SHA1 file checksum
+ * - 24-byte entries * nr (object ID + 4-byte offset)
+ * - hash of the packfile
+ * - file checksum
*/
- if (idx_size != 4*256 + nr * 24 + 20 + 20) {
+ if (idx_size != 4*256 + nr * (hashsz + 4) + hashsz + hashsz) {
munmap(idx_map, idx_size);
return error("wrong index v1 file size in %s", path);
}
* Minimum size:
* - 8 bytes of header
* - 256 index entries 4 bytes each
- * - 20-byte sha1 entry * nr
+ * - object ID entry * nr
* - 4-byte crc entry * nr
* - 4-byte offset entry * nr
- * - 20-byte SHA1 of the packfile
- * - 20-byte SHA1 file checksum
+ * - hash of the packfile
+ * - file checksum
* And after the 4-byte offset table might be a
* variable sized table containing 8-byte entries
* for offsets larger than 2^31.
*/
- unsigned long min_size = 8 + 4*256 + nr*(20 + 4 + 4) + 20 + 20;
+ unsigned long min_size = 8 + 4*256 + nr*(hashsz + 4 + 4) + hashsz + hashsz;
unsigned long max_size = min_size;
if (nr)
max_size += (nr - 1)*8;
return 0;
if (!strip_suffix(p->pack_name, ".pack", &len))
- die("BUG: pack_name does not end in .pack");
+ BUG("pack_name does not end in .pack");
idx_name = xstrfmt("%.*s.idx", (int)len, p->pack_name);
ret = check_packed_git_idx(idx_name, p);
free(idx_name);
for (p = o->packed_git; p; p = p->next)
if (p->do_not_close)
- die("BUG: want to close pack marked 'do-not-close'");
+ BUG("want to close pack marked 'do-not-close'");
else
close_pack(p);
}
{
struct stat st;
struct pack_header hdr;
- unsigned char sha1[20];
- unsigned char *idx_sha1;
+ unsigned char hash[GIT_MAX_RAWSZ];
+ unsigned char *idx_hash;
long fd_flag;
ssize_t read_result;
+ const unsigned hashsz = the_hash_algo->rawsz;
if (!p->index_data && open_pack_index(p))
return error("packfile %s index unavailable", p->pack_name);
" while index indicates %"PRIu32" objects",
p->pack_name, ntohl(hdr.hdr_entries),
p->num_objects);
- if (lseek(p->pack_fd, p->pack_size - sizeof(sha1), SEEK_SET) == -1)
+ if (lseek(p->pack_fd, p->pack_size - hashsz, SEEK_SET) == -1)
return error("end of packfile %s is unavailable", p->pack_name);
- read_result = read_in_full(p->pack_fd, sha1, sizeof(sha1));
+ read_result = read_in_full(p->pack_fd, hash, hashsz);
if (read_result < 0)
return error_errno("error reading from %s", p->pack_name);
- if (read_result != sizeof(sha1))
+ if (read_result != hashsz)
return error("packfile %s signature is unavailable", p->pack_name);
- idx_sha1 = ((unsigned char *)p->index_data) + p->index_size - 40;
- if (hashcmp(sha1, idx_sha1))
+ idx_hash = ((unsigned char *)p->index_data) + p->index_size - hashsz * 2;
+ if (hashcmp(hash, idx_hash))
return error("packfile %s does not match index", p->pack_name);
return 0;
}
static int in_window(struct pack_window *win, off_t offset)
{
- /* We must promise at least 20 bytes (one hash) after the
+ /* We must promise at least one full hash after the
* offset is available from this window, otherwise the offset
* is not actually in this window and a different window (which
* has that one hash excess) must be used. This is to support
*/
off_t win_off = win->offset;
return win_off <= offset
- && (offset + 20) <= (win_off + win->len);
+ && (offset + the_hash_algo->rawsz) <= (win_off + win->len);
}
unsigned char *use_pack(struct packed_git *p,
*/
if (!p->pack_size && p->pack_fd == -1 && open_packed_git(p))
die("packfile %s cannot be accessed", p->pack_name);
- if (offset > (p->pack_size - 20))
+ if (offset > (p->pack_size - the_hash_algo->rawsz))
die("offset beyond end of packfile (truncated pack?)");
if (offset < 0)
die(_("offset before end of packfile (broken .idx?)"));
p->pack_size = st.st_size;
p->pack_local = local;
p->mtime = st.st_mtime;
- if (path_len < 40 || get_sha1_hex(path + path_len - 40, p->sha1))
+ if (path_len < the_hash_algo->hexsz ||
+ get_sha1_hex(path + path_len - the_hash_algo->hexsz, p->sha1))
hashclr(p->sha1);
return p;
}
for (p = the_repository->objects->packed_git; p; p = p->next)
for (i = 0; i < p->num_bad_objects; i++)
- if (!hashcmp(sha1, p->bad_object_sha1 + 20 * i))
+ if (!hashcmp(sha1,
+ p->bad_object_sha1 + the_hash_algo->rawsz * i))
return p;
return NULL;
}
} else if (type == OBJ_REF_DELTA) {
/* The base entry _must_ be in the same pack */
base_offset = find_pack_entry_one(base_info, p);
- *curpos += 20;
+ *curpos += the_hash_algo->rawsz;
} else
die("I am totally screwed");
return base_offset;
return NULL;
}
-static int retry_bad_packed_offset(struct packed_git *p, off_t obj_offset)
+static int retry_bad_packed_offset(struct repository *r,
+ struct packed_git *p,
+ off_t obj_offset)
{
int type;
struct revindex_entry *revidx;
return OBJ_BAD;
nth_packed_object_oid(&oid, p, revidx->nr);
mark_bad_packed_object(p, oid.hash);
- type = oid_object_info(&oid, NULL);
+ type = oid_object_info(r, &oid, NULL);
if (type <= OBJ_NONE)
return OBJ_BAD;
return type;
#define POI_STACK_PREALLOC 64
-static enum object_type packed_to_object_type(struct packed_git *p,
+static enum object_type packed_to_object_type(struct repository *r,
+ struct packed_git *p,
off_t obj_offset,
enum object_type type,
struct pack_window **w_curs,
if (type <= OBJ_NONE) {
/* If getting the base itself fails, we first
* retry the base, otherwise unwind */
- type = retry_bad_packed_offset(p, base_offset);
+ type = retry_bad_packed_offset(r, p, base_offset);
if (type > OBJ_NONE)
goto out;
goto unwind;
unwind:
while (poi_stack_nr) {
obj_offset = poi_stack[--poi_stack_nr];
- type = retry_bad_packed_offset(p, obj_offset);
+ type = retry_bad_packed_offset(r, p, obj_offset);
if (type > OBJ_NONE)
goto out;
}
free(ent);
}
-static void *cache_or_unpack_entry(struct packed_git *p, off_t base_offset,
- unsigned long *base_size, enum object_type *type)
+static void *cache_or_unpack_entry(struct repository *r, struct packed_git *p,
+ off_t base_offset, unsigned long *base_size,
+ enum object_type *type)
{
struct delta_base_cache_entry *ent;
ent = get_delta_base_cache_entry(p, base_offset);
if (!ent)
- return unpack_entry(p, base_offset, type, base_size);
+ return unpack_entry(r, p, base_offset, type, base_size);
if (type)
*type = ent->type;
hashmap_add(&delta_base_cache, ent);
}
-int packed_object_info(struct packed_git *p, off_t obj_offset,
- struct object_info *oi)
+int packed_object_info(struct repository *r, struct packed_git *p,
+ off_t obj_offset, struct object_info *oi)
{
struct pack_window *w_curs = NULL;
unsigned long size;
* a "real" type later if the caller is interested.
*/
if (oi->contentp) {
- *oi->contentp = cache_or_unpack_entry(p, obj_offset, oi->sizep,
+ *oi->contentp = cache_or_unpack_entry(r, p, obj_offset, oi->sizep,
&type);
if (!*oi->contentp)
type = OBJ_BAD;
if (oi->typep || oi->type_name) {
enum object_type ptot;
- ptot = packed_to_object_type(p, obj_offset, type, &w_curs,
- curpos);
+ ptot = packed_to_object_type(r, p, obj_offset,
+ type, &w_curs, curpos);
if (oi->typep)
*oi->typep = ptot;
if (oi->type_name) {
unsigned long size;
};
-static void *read_object(const struct object_id *oid, enum object_type *type,
+static void *read_object(struct repository *r,
+ const struct object_id *oid,
+ enum object_type *type,
unsigned long *size)
{
struct object_info oi = OBJECT_INFO_INIT;
oi.sizep = size;
oi.contentp = &content;
- if (oid_object_info_extended(oid, &oi, 0) < 0)
+ if (oid_object_info_extended(r, oid, &oi, 0) < 0)
return NULL;
return content;
}
-void *unpack_entry(struct packed_git *p, off_t obj_offset,
+void *unpack_entry(struct repository *r, struct packed_git *p, off_t obj_offset,
enum object_type *final_type, unsigned long *final_size)
{
struct pack_window *w_curs = NULL;
case OBJ_OFS_DELTA:
case OBJ_REF_DELTA:
if (data)
- die("BUG: unpack_entry: left loop at a valid delta");
+ BUG("unpack_entry: left loop at a valid delta");
break;
case OBJ_COMMIT:
case OBJ_TREE:
oid_to_hex(&base_oid), (uintmax_t)obj_offset,
p->pack_name);
mark_bad_packed_object(p, base_oid.hash);
- base = read_object(&base_oid, &type, &base_size);
+ base = read_object(r, &base_oid, &type, &base_size);
external_base = base;
}
}
{
const unsigned char *index_fanout = p->index_data;
const unsigned char *index_lookup;
+ const unsigned int hashsz = the_hash_algo->rawsz;
int index_lookup_width;
if (!index_fanout)
index_lookup = index_fanout + 4 * 256;
if (p->index_version == 1) {
- index_lookup_width = 24;
+ index_lookup_width = hashsz + 4;
index_lookup += 4;
} else {
- index_lookup_width = 20;
+ index_lookup_width = hashsz;
index_fanout += 8;
index_lookup += 8;
}
uint32_t n)
{
const unsigned char *index = p->index_data;
+ const unsigned int hashsz = the_hash_algo->rawsz;
if (!index) {
if (open_pack_index(p))
return NULL;
return NULL;
index += 4 * 256;
if (p->index_version == 1) {
- return index + 24 * n + 4;
+ return index + (hashsz + 4) * n + 4;
} else {
index += 8;
- return index + 20 * n;
+ return index + hashsz * n;
}
}
off_t nth_packed_object_offset(const struct packed_git *p, uint32_t n)
{
const unsigned char *index = p->index_data;
+ const unsigned int hashsz = the_hash_algo->rawsz;
index += 4 * 256;
if (p->index_version == 1) {
- return ntohl(*((uint32_t *)(index + 24 * n)));
+ return ntohl(*((uint32_t *)(index + (hashsz + 4) * n)));
} else {
uint32_t off;
- index += 8 + p->num_objects * (20 + 4);
+ index += 8 + p->num_objects * (hashsz + 4);
off = ntohl(*((uint32_t *)(index + 4 * n)));
if (!(off & 0x80000000))
return off;
}
-static int fill_pack_entry(const unsigned char *sha1,
+static int fill_pack_entry(const struct object_id *oid,
struct pack_entry *e,
struct packed_git *p)
{
if (p->num_bad_objects) {
unsigned i;
for (i = 0; i < p->num_bad_objects; i++)
- if (!hashcmp(sha1, p->bad_object_sha1 + 20 * i))
+ if (!hashcmp(oid->hash,
+ p->bad_object_sha1 + the_hash_algo->rawsz * i))
return 0;
}
- offset = find_pack_entry_one(sha1, p);
+ offset = find_pack_entry_one(oid->hash, p);
if (!offset)
return 0;
return 0;
e->offset = offset;
e->p = p;
- hashcpy(e->sha1, sha1);
return 1;
}
-int find_pack_entry(struct repository *r, const unsigned char *sha1, struct pack_entry *e)
+int find_pack_entry(struct repository *r, const struct object_id *oid, struct pack_entry *e)
{
struct list_head *pos;
list_for_each(pos, &r->objects->packed_git_mru) {
struct packed_git *p = list_entry(pos, struct packed_git, mru);
- if (fill_pack_entry(sha1, e, p)) {
+ if (fill_pack_entry(oid, e, p)) {
list_move(&p->mru, &r->objects->packed_git_mru);
return 1;
}
return 0;
}
-int has_sha1_pack(const unsigned char *sha1)
+int has_object_pack(const struct object_id *oid)
{
struct pack_entry e;
- return find_pack_entry(the_repository, sha1, &e);
+ return find_pack_entry(the_repository, oid, &e);
}
int has_pack_index(const unsigned char *sha1)
struct commit *commit = (struct commit *) obj;
struct commit_list *parents = commit->parents;
- oidset_insert(set, &commit->tree->object.oid);
+ oidset_insert(set, get_commit_tree_oid(commit));
for (; parents; parents = parents->next)
oidset_insert(set, &parents->item->object.oid);
} else if (obj->type == OBJ_TAG) {
extern off_t find_pack_entry_one(const unsigned char *sha1, struct packed_git *);
extern int is_pack_valid(struct packed_git *);
-extern void *unpack_entry(struct packed_git *, off_t, enum object_type *, unsigned long *);
+extern void *unpack_entry(struct repository *r, struct packed_git *, off_t, enum object_type *, unsigned long *);
extern unsigned long unpack_object_header_buffer(const unsigned char *buf, unsigned long len, enum object_type *type, unsigned long *sizep);
extern unsigned long get_size_from_delta(struct packed_git *, struct pack_window **, off_t);
extern int unpack_object_header(struct packed_git *, struct pack_window **, off_t *, unsigned long *);
/* global flag to enable extra checks when accessing packed objects */
extern int do_check_packed_object_crc;
-extern int packed_object_info(struct packed_git *pack, off_t offset, struct object_info *);
+extern int packed_object_info(struct repository *r,
+ struct packed_git *pack,
+ off_t offset, struct object_info *);
extern void mark_bad_packed_object(struct packed_git *p, const unsigned char *sha1);
extern const struct packed_git *has_packed_and_bad(const unsigned char *sha1);
* Iff a pack file in the given repository contains the object named by sha1,
* return true and store its location to e.
*/
-extern int find_pack_entry(struct repository *r, const unsigned char *sha1, struct pack_entry *e);
+extern int find_pack_entry(struct repository *r, const struct object_id *oid, struct pack_entry *e);
-extern int has_sha1_pack(const unsigned char *sha1);
+extern int has_object_pack(const struct object_id *oid);
extern int has_pack_index(const unsigned char *sha1);
return;
/*
- * force computing the width of the terminal before we redirect
- * the standard output to the pager.
+ * After we redirect standard output, we won't be able to use an ioctl
+ * to get the terminal size. Let's grab it now, and then set $COLUMNS
+ * to communicate it to any sub-processes.
*/
- (void) term_columns();
+ {
+ char buf[64];
+ xsnprintf(buf, sizeof(buf), "%d", term_columns());
+ setenv("COLUMNS", buf, 0);
+ }
setenv("GIT_PAGER_IN_USE", "true", 1);
int is_ntfs_dotgit(const char *name)
{
- int len;
+ size_t len;
for (len = 0; ; len++)
if (!name[len] || name[len] == '\\' || is_dir_sep(name[len])) {
}
}
+static int is_ntfs_dot_generic(const char *name,
+ const char *dotgit_name,
+ size_t len,
+ const char *dotgit_ntfs_shortname_prefix)
+{
+ int saw_tilde;
+ size_t i;
+
+ if ((name[0] == '.' && !strncasecmp(name + 1, dotgit_name, len))) {
+ i = len + 1;
+only_spaces_and_periods:
+ for (;;) {
+ char c = name[i++];
+ if (!c)
+ return 1;
+ if (c != ' ' && c != '.')
+ return 0;
+ }
+ }
+
+ /*
+ * Is it a regular NTFS short name, i.e. shortened to 6 characters,
+ * followed by ~1, ... ~4?
+ */
+ if (!strncasecmp(name, dotgit_name, 6) && name[6] == '~' &&
+ name[7] >= '1' && name[7] <= '4') {
+ i = 8;
+ goto only_spaces_and_periods;
+ }
+
+ /*
+ * Is it a fall-back NTFS short name (for details, see
+ * https://en.wikipedia.org/wiki/8.3_filename?
+ */
+ for (i = 0, saw_tilde = 0; i < 8; i++)
+ if (name[i] == '\0')
+ return 0;
+ else if (saw_tilde) {
+ if (name[i] < '0' || name[i] > '9')
+ return 0;
+ } else if (name[i] == '~') {
+ if (name[++i] < '1' || name[i] > '9')
+ return 0;
+ saw_tilde = 1;
+ } else if (i >= 6)
+ return 0;
+ else if (name[i] < 0) {
+ /*
+ * We know our needles contain only ASCII, so we clamp
+ * here to make the results of tolower() sane.
+ */
+ return 0;
+ } else if (tolower(name[i]) != dotgit_ntfs_shortname_prefix[i])
+ return 0;
+
+ goto only_spaces_and_periods;
+}
+
+/*
+ * Inline helper to make sure compiler resolves strlen() on literals at
+ * compile time.
+ */
+static inline int is_ntfs_dot_str(const char *name, const char *dotgit_name,
+ const char *dotgit_ntfs_shortname_prefix)
+{
+ return is_ntfs_dot_generic(name, dotgit_name, strlen(dotgit_name),
+ dotgit_ntfs_shortname_prefix);
+}
+
+int is_ntfs_dotgitmodules(const char *name)
+{
+ return is_ntfs_dot_str(name, "gitmodules", "gi7eba");
+}
+
+int is_ntfs_dotgitignore(const char *name)
+{
+ return is_ntfs_dot_str(name, "gitignore", "gi250a");
+}
+
+int is_ntfs_dotgitattributes(const char *name)
+{
+ return is_ntfs_dot_str(name, "gitattributes", "gi7d29");
+}
+
int looks_like_command_line_option(const char *str)
{
return str && str[0] == '-';
}
if (item->attr_check->nr != item->attr_match_nr)
- die("BUG: should have same number of entries");
+ BUG("should have same number of entries");
string_list_clear(&list, 0);
}
if (pathspec_prefix >= 0 &&
(prefixlen || (prefix && *prefix)))
- die("BUG: 'prefix' magic is supposed to be used at worktree's root");
+ BUG("'prefix' magic is supposed to be used at worktree's root");
if ((magic & PATHSPEC_LITERAL) && (magic & PATHSPEC_GLOB))
die(_("%s: 'literal' and 'glob' are incompatible"), elt);
/* sanity checks, pathspec matchers assume these are sane */
if (item->nowildcard_len > item->len ||
item->prefix > item->len) {
- die ("BUG: error initializing pathspec_item");
+ BUG("error initializing pathspec_item");
}
}
if ((flags & PATHSPEC_PREFER_CWD) &&
(flags & PATHSPEC_PREFER_FULL))
- die("BUG: PATHSPEC_PREFER_CWD and PATHSPEC_PREFER_FULL are incompatible");
+ BUG("PATHSPEC_PREFER_CWD and PATHSPEC_PREFER_FULL are incompatible");
/* No arguments with prefix -> prefix pathspec */
if (!entry) {
return;
if (!(flags & PATHSPEC_PREFER_CWD))
- die("BUG: PATHSPEC_PREFER_CWD requires arguments");
+ BUG("PATHSPEC_PREFER_CWD requires arguments");
pathspec->items = item = xcalloc(1, sizeof(*item));
item->match = xstrdup(prefix);
if (pathspec->magic & PATHSPEC_MAXDEPTH) {
if (flags & PATHSPEC_KEEP_ORDER)
- die("BUG: PATHSPEC_MAXDEPTH_VALID and PATHSPEC_KEEP_ORDER are incompatible");
+ BUG("PATHSPEC_MAXDEPTH_VALID and PATHSPEC_KEEP_ORDER are incompatible");
QSORT(pathspec->items, pathspec->nr, pathspec_item_cmp);
}
}
ssize_t ret;
if (fd >= 0 && src_buf && *src_buf)
- die("BUG: multiple sources given to packet_read");
+ BUG("multiple sources given to packet_read");
/* Read up to "size" bytes from our source, whatever it is. */
if (src_buf && *src_buf) {
strbuf_addstr(sb, diff_get_color(c->auto_color, DIFF_RESET));
return 1;
case 'T': /* tree hash */
- strbuf_addstr(sb, oid_to_hex(&commit->tree->object.oid));
+ strbuf_addstr(sb, oid_to_hex(get_commit_tree_oid(commit)));
return 1;
case 't': /* abbreviated tree hash */
- strbuf_add_unique_abbrev(sb, &commit->tree->object.oid,
+ strbuf_add_unique_abbrev(sb,
+ get_commit_tree_oid(commit),
c->pretty_ctx->abbrev);
return 1;
case 'P': /* parent hashes */
int i, j;
if (queue->compare != NULL)
- die("BUG: prio_queue_reverse() on non-LIFO queue");
+ BUG("prio_queue_reverse() on non-LIFO queue");
for (i = 0; i < (j = (queue->nr - 1) - i); i++)
swap(queue, i, j);
}
* later processing, and the revision machinery expects
* commits and tags to have been parsed.
*/
- type = oid_object_info(oid, NULL);
+ type = oid_object_info(the_repository, oid, NULL);
if (type < 0)
die("unable to get object info for %s", oid_to_hex(oid));
int size, len;
struct cache_entry *ce, *ret;
- if (!verify_path(path)) {
+ if (!verify_path(path, mode)) {
error("Invalid path '%s'", path);
return NULL;
}
* Also, we don't want double slashes or slashes at the
* end that can make pathnames ambiguous.
*/
-static int verify_dotfile(const char *rest)
+static int verify_dotfile(const char *rest, unsigned mode)
{
/*
* The first character was '.', but that
switch (*rest) {
/*
- * ".git" followed by NUL or slash is bad. This
- * shares the path end test with the ".." case.
+ * ".git" followed by NUL or slash is bad. Note that we match
+ * case-insensitively here, even if ignore_case is not set.
+ * This outlaws ".GIT" everywhere out of an abundance of caution,
+ * since there's really no good reason to allow it.
+ *
+ * Once we've seen ".git", we can also find ".gitmodules", etc (also
+ * case-insensitively).
*/
case 'g':
case 'G':
break;
if (rest[2] != 't' && rest[2] != 'T')
break;
- rest += 2;
- /* fallthrough */
+ if (rest[3] == '\0' || is_dir_sep(rest[3]))
+ return 0;
+ if (S_ISLNK(mode)) {
+ rest += 3;
+ if (skip_iprefix(rest, "modules", &rest) &&
+ (*rest == '\0' || is_dir_sep(*rest)))
+ return 0;
+ }
+ break;
case '.':
if (rest[1] == '\0' || is_dir_sep(rest[1]))
return 0;
return 1;
}
-int verify_path(const char *path)
+int verify_path(const char *path, unsigned mode)
{
char c;
return 1;
if (is_dir_sep(c)) {
inside:
- if (protect_hfs && is_hfs_dotgit(path))
- return 0;
- if (protect_ntfs && is_ntfs_dotgit(path))
- return 0;
+ if (protect_hfs) {
+ if (is_hfs_dotgit(path))
+ return 0;
+ if (S_ISLNK(mode)) {
+ if (is_hfs_dotgitmodules(path))
+ return 0;
+ }
+ }
+ if (protect_ntfs) {
+ if (is_ntfs_dotgit(path))
+ return 0;
+ if (S_ISLNK(mode)) {
+ if (is_ntfs_dotgitmodules(path))
+ return 0;
+ }
+ }
+
c = *path++;
- if ((c == '.' && !verify_dotfile(path)) ||
+ if ((c == '.' && !verify_dotfile(path, mode)) ||
is_dir_sep(c) || c == '\0')
return 0;
}
if (!ok_to_add)
return -1;
- if (!verify_path(ce->name))
+ if (!verify_path(ce->name, ce->ce_mode))
return error("Invalid path '%s'", ce->name);
if (!skip_df_check &&
if (verify_hdr(hdr, mmap_size) < 0)
goto unmap;
- hashcpy(istate->sha1, (const unsigned char *)hdr + mmap_size - the_hash_algo->rawsz);
+ hashcpy(istate->oid.hash, (const unsigned char *)hdr + mmap_size - the_hash_algo->rawsz);
istate->version = ntohl(hdr->hdr_version);
istate->cache_nr = ntohl(hdr->hdr_entries);
istate->cache_alloc = alloc_nr(istate->cache_nr);
uint64_t start = getnanotime();
struct split_index *split_index;
int ret;
- char *base_sha1_hex;
+ char *base_oid_hex;
char *base_path;
/* istate->initialized covers both .git/index and .git/sharedindex.xxx */
trace_performance_since(start, "read cache %s", path);
split_index = istate->split_index;
- if (!split_index || is_null_sha1(split_index->base_sha1)) {
+ if (!split_index || is_null_oid(&split_index->base_oid)) {
post_read_index_from(istate);
return ret;
}
else
split_index->base = xcalloc(1, sizeof(*split_index->base));
- base_sha1_hex = sha1_to_hex(split_index->base_sha1);
- base_path = xstrfmt("%s/sharedindex.%s", gitdir, base_sha1_hex);
+ base_oid_hex = oid_to_hex(&split_index->base_oid);
+ base_path = xstrfmt("%s/sharedindex.%s", gitdir, base_oid_hex);
ret = do_read_index(split_index->base, base_path, 1);
- if (hashcmp(split_index->base_sha1, split_index->base->sha1))
+ if (oidcmp(&split_index->base_oid, &split_index->base->oid))
die("broken index, expect %s in %s, got %s",
- base_sha1_hex, base_path,
- sha1_to_hex(split_index->base->sha1));
+ base_oid_hex, base_path,
+ oid_to_hex(&split_index->base->oid));
freshen_shared_index(base_path, 0);
merge_base_index(istate);
if (n != the_hash_algo->rawsz)
goto out;
- if (hashcmp(istate->sha1, hash))
+ if (hashcmp(istate->oid.hash, hash))
goto out;
close(fd);
if (!istate->version) {
istate->version = get_index_format_default();
- if (getenv("GIT_TEST_SPLIT_INDEX"))
+ if (git_env_bool("GIT_TEST_SPLIT_INDEX", 0))
init_split_index(istate);
}
return -1;
}
- if (ce_flush(&c, newfd, istate->sha1))
+ if (ce_flush(&c, newfd, istate->oid.hash))
return -1;
if (close_tempfile_gently(tempfile)) {
error(_("could not close '%s'"), tempfile->filename.buf);
return ret;
}
ret = rename_tempfile(temp,
- git_path("sharedindex.%s", sha1_to_hex(si->base->sha1)));
+ git_path("sharedindex.%s", oid_to_hex(&si->base->oid)));
if (!ret) {
- hashcpy(si->base_sha1, si->base->sha1);
- clean_shared_index_files(sha1_to_hex(si->base->sha1));
+ oidcpy(&si->base_oid, &si->base->oid);
+ clean_shared_index_files(oid_to_hex(&si->base->oid));
}
return ret;
if (!si || alternate_index_output ||
(istate->cache_changed & ~EXTMASK)) {
if (si)
- hashclr(si->base_sha1);
+ oidclr(&si->base_oid);
ret = do_write_locked_index(istate, lock, flags);
goto out;
}
- if (getenv("GIT_TEST_SPLIT_INDEX")) {
- int v = si->base_sha1[0];
+ if (git_env_bool("GIT_TEST_SPLIT_INDEX", 0)) {
+ int v = si->base_oid.hash[0];
if ((v & 15) < 6)
istate->cache_changed |= SPLIT_INDEX_ORDERED;
}
temp = mks_tempfile(git_path("sharedindex_XXXXXX"));
if (!temp) {
- hashclr(si->base_sha1);
+ oidclr(&si->base_oid);
ret = do_write_locked_index(istate, lock, flags);
goto out;
}
/* Freshen the shared index only if the split-index was written */
if (!ret && !new_shared_index) {
const char *shared_index = git_path("sharedindex.%s",
- sha1_to_hex(si->base_sha1));
+ oid_to_hex(&si->base_oid));
freshen_shared_index(shared_index, 1);
}
v->s = xstrdup(find_unique_abbrev(oid, atom->u.objectname.length));
return 1;
} else
- die("BUG: unknown %%(objectname) option");
+ BUG("unknown %%(objectname) option");
}
return 0;
}
if (deref)
name++;
if (!strcmp(name, "tree")) {
- v->s = xstrdup(oid_to_hex(&commit->tree->object.oid));
+ v->s = xstrdup(oid_to_hex(get_commit_tree_oid(commit)));
}
else if (!strcmp(name, "numparent")) {
v->value = commit_list_count(commit->parents);
else
*s = "";
} else
- die("BUG: unhandled RR_* enum");
+ BUG("unhandled RR_* enum");
}
char *get_head_description(void)
struct object *o = lookup_unknown_object(name->hash);
if (o->type == OBJ_NONE) {
- int type = oid_object_info(name, NULL);
+ int type = oid_object_info(the_repository, name, NULL);
if (type < 0 || !object_as_type(o, type, 0))
return PEEL_INVALID;
}
static int is_per_worktree_ref(const char *refname)
{
return !strcmp(refname, "HEAD") ||
- starts_with(refname, "refs/bisect/");
+ starts_with(refname, "refs/bisect/") ||
+ starts_with(refname, "refs/rewritten/");
}
static int is_pseudoref_syntax(const char *refname)
{
const char *filename;
int fd;
- static struct lock_file lock;
+ struct lock_file lock = LOCK_INIT;
struct strbuf buf = STRBUF_INIT;
int ret = -1;
strbuf_addf(&buf, "%s\n", oid_to_hex(oid));
filename = git_path("%s", pseudoref);
- fd = hold_lock_file_for_update_timeout(&lock, filename,
- LOCK_DIE_ON_ERROR,
+ fd = hold_lock_file_for_update_timeout(&lock, filename, 0,
get_files_ref_lock_timeout_ms());
if (fd < 0) {
strbuf_addf(err, "could not open '%s' for writing: %s",
if (old_oid) {
struct object_id actual_old_oid;
- if (read_ref(pseudoref, &actual_old_oid))
- die("could not read ref '%s'", pseudoref);
- if (oidcmp(&actual_old_oid, old_oid)) {
- strbuf_addf(err, "unexpected sha1 when writing '%s'", pseudoref);
+ if (read_ref(pseudoref, &actual_old_oid)) {
+ if (!is_null_oid(old_oid)) {
+ strbuf_addf(err, "could not read ref '%s'",
+ pseudoref);
+ rollback_lock_file(&lock);
+ goto done;
+ }
+ } else if (is_null_oid(old_oid)) {
+ strbuf_addf(err, "ref '%s' already exists",
+ pseudoref);
+ rollback_lock_file(&lock);
+ goto done;
+ } else if (oidcmp(&actual_old_oid, old_oid)) {
+ strbuf_addf(err, "unexpected object ID when writing '%s'",
+ pseudoref);
rollback_lock_file(&lock);
goto done;
}
static int delete_pseudoref(const char *pseudoref, const struct object_id *old_oid)
{
- static struct lock_file lock;
const char *filename;
filename = git_path("%s", pseudoref);
if (old_oid && !is_null_oid(old_oid)) {
+ struct lock_file lock = LOCK_INIT;
int fd;
struct object_id actual_old_oid;
fd = hold_lock_file_for_update_timeout(
- &lock, filename, LOCK_DIE_ON_ERROR,
+ &lock, filename, 0,
get_files_ref_lock_timeout_ms());
- if (fd < 0)
- die_errno(_("Could not open '%s' for writing"), filename);
+ if (fd < 0) {
+ error_errno(_("could not open '%s' for writing"),
+ filename);
+ return -1;
+ }
if (read_ref(pseudoref, &actual_old_oid))
die("could not read ref '%s'", pseudoref);
if (oidcmp(&actual_old_oid, old_oid)) {
- warning("Unexpected sha1 when deleting %s", pseudoref);
+ error("unexpected object ID when deleting '%s'",
+ pseudoref);
rollback_lock_file(&lock);
return -1;
}
/* OK */
break;
case REF_TRANSACTION_PREPARED:
- die("BUG: free called on a prepared reference transaction");
+ BUG("free called on a prepared reference transaction");
break;
default:
- die("BUG: unexpected reference transaction state");
+ BUG("unexpected reference transaction state");
break;
}
struct ref_update *update;
if (transaction->state != REF_TRANSACTION_OPEN)
- die("BUG: update called for transaction that is not open");
+ BUG("update called for transaction that is not open");
FLEX_ALLOC_STR(update, refname, refname);
ALLOC_GROW(transaction->updates, transaction->nr + 1, transaction->alloc);
struct strbuf *err)
{
if (!new_oid || is_null_oid(new_oid))
- die("BUG: create called without valid new_oid");
+ BUG("create called without valid new_oid");
return ref_transaction_update(transaction, refname, new_oid,
&null_oid, flags, msg, err);
}
struct strbuf *err)
{
if (old_oid && is_null_oid(old_oid))
- die("BUG: delete called with old_oid set to zeros");
+ BUG("delete called with old_oid set to zeros");
return ref_transaction_update(transaction, refname,
&null_oid, old_oid,
flags, msg, err);
struct strbuf *err)
{
if (!old_oid)
- die("BUG: verify called with old_oid set to NULL");
+ BUG("verify called with old_oid set to NULL");
return ref_transaction_update(transaction, refname,
NULL, old_oid,
flags, NULL, err);
struct ref_store *refs;
if (!be)
- die("BUG: reference backend %s is unknown", be_name);
+ BUG("reference backend %s is unknown", be_name);
refs = be->init(gitdir, flags);
return refs;
if (r->refs)
return r->refs;
+ if (!r->gitdir)
+ BUG("attempting to get main_ref_store outside of repository");
+
r->refs = ref_store_init(r->gitdir, REF_STORE_ALL_CAPS);
return r->refs;
}
hashmap_init(map, ref_store_hash_cmp, NULL, 0);
if (hashmap_put(map, alloc_ref_store_hash_entry(name, refs)))
- die("BUG: %s ref_store '%s' initialized twice", type, name);
+ BUG("%s ref_store '%s' initialized twice", type, name);
}
struct ref_store *get_submodule_ref_store(const char *submodule)
refnames->items[i].string);
return 1;
} else if (cmp > 0) {
- die("BUG: ref_update_reject_duplicates() received unsorted list");
+ BUG("ref_update_reject_duplicates() received unsorted list");
}
}
return 0;
/* Good. */
break;
case REF_TRANSACTION_PREPARED:
- die("BUG: prepare called twice on reference transaction");
+ BUG("prepare called twice on reference transaction");
break;
case REF_TRANSACTION_CLOSED:
- die("BUG: prepare called on a closed reference transaction");
+ BUG("prepare called on a closed reference transaction");
break;
default:
- die("BUG: unexpected reference transaction state");
+ BUG("unexpected reference transaction state");
break;
}
ret = refs->be->transaction_abort(refs, transaction, err);
break;
case REF_TRANSACTION_CLOSED:
- die("BUG: abort called on a closed reference transaction");
+ BUG("abort called on a closed reference transaction");
break;
default:
- die("BUG: unexpected reference transaction state");
+ BUG("unexpected reference transaction state");
break;
}
/* Fall through to finish. */
break;
case REF_TRANSACTION_CLOSED:
- die("BUG: commit called on a closed reference transaction");
+ BUG("commit called on a closed reference transaction");
break;
default:
- die("BUG: unexpected reference transaction state");
+ BUG("unexpected reference transaction state");
break;
}
}
if (ok != ITER_DONE)
- die("BUG: error while iterating over references");
+ BUG("error while iterating over references");
extra_refname = find_descendant_ref(dirname.buf, extras, skip);
if (extra_refname)
if (refs->store_flags & REF_STORE_MAIN)
return;
- die("BUG: operation %s only allowed for main ref store", caller);
+ BUG("operation %s only allowed for main ref store", caller);
}
/*
struct files_ref_store *refs;
if (ref_store->be != &refs_be_files)
- die("BUG: ref_store is type \"%s\" not \"files\" in %s",
+ BUG("ref_store is type \"%s\" not \"files\" in %s",
ref_store->be->name, caller);
refs = (struct files_ref_store *)ref_store;
if ((refs->store_flags & required_flags) != required_flags)
- die("BUG: operation %s requires abilities 0x%x, but only have 0x%x",
+ BUG("operation %s requires abilities 0x%x, but only have 0x%x",
caller, required_flags, refs->store_flags);
return refs;
strbuf_addf(sb, "%s/logs/%s", refs->gitcommondir, refname);
break;
default:
- die("BUG: unknown ref type %d of ref %s",
+ BUG("unknown ref type %d of ref %s",
ref_type(refname), refname);
}
}
strbuf_addf(sb, "%s/%s", refs->gitcommondir, refname);
break;
default:
- die("BUG: unknown ref type %d of ref %s",
+ BUG("unknown ref type %d of ref %s",
ref_type(refname), refname);
}
}
}
if (!ret && sb.len)
- die("BUG: reverse reflog parser had leftover data");
+ BUG("reverse reflog parser had leftover data");
fclose(logfp);
strbuf_release(&sb);
static int files_reflog_iterator_peel(struct ref_iterator *ref_iterator,
struct object_id *peeled)
{
- die("BUG: ref_iterator_peel() called for reflog_iterator");
+ BUG("ref_iterator_peel() called for reflog_iterator");
}
static int files_reflog_iterator_abort(struct ref_iterator *ref_iterator)
assert(err);
if (transaction->state != REF_TRANSACTION_OPEN)
- die("BUG: commit called for transaction that is not open");
+ BUG("commit called for transaction that is not open");
/* Fail if a refname appears more than once in the transaction: */
for (i = 0; i < transaction->nr; i++)
*/
if (refs_for_each_rawref(&refs->base, ref_present,
&affected_refnames))
- die("BUG: initial ref transaction called with existing refs");
+ BUG("initial ref transaction called with existing refs");
packed_transaction = ref_store_transaction_begin(refs->packed_ref_store, err);
if (!packed_transaction) {
if ((update->flags & REF_HAVE_OLD) &&
!is_null_oid(&update->old_oid))
- die("BUG: initial ref transaction with old_sha1 set");
+ BUG("initial ref transaction with old_sha1 set");
if (refs_verify_refname_available(&refs->base, update->refname,
&affected_refnames, NULL,
err)) {
{
struct files_ref_store *refs =
files_downcast(ref_store, REF_STORE_WRITE, "reflog_expire");
- static struct lock_file reflog_lock;
+ struct lock_file reflog_lock = LOCK_INIT;
struct expire_reflog_cb cb;
struct ref_lock *lock;
struct strbuf log_file_sb = STRBUF_INIT;
static int empty_ref_iterator_peel(struct ref_iterator *ref_iterator,
struct object_id *peeled)
{
- die("BUG: peel called for empty iterator");
+ BUG("peel called for empty iterator");
}
static int empty_ref_iterator_abort(struct ref_iterator *ref_iterator)
(struct merge_ref_iterator *)ref_iterator;
if (!iter->current) {
- die("BUG: peel called before advance for merge iterator");
+ BUG("peel called before advance for merge iterator");
}
return ref_iterator_peel(*iter->current, peeled);
}
* trimming, report it as a bug:
*/
if (strlen(iter->iter0->refname) <= iter->trim)
- die("BUG: attempt to trim too many characters");
+ BUG("attempt to trim too many characters");
iter->base.refname = iter->iter0->refname + iter->trim;
} else {
iter->base.refname = iter->iter0->refname;
struct packed_ref_store *refs;
if (ref_store->be != &refs_be_packed)
- die("BUG: ref_store is type \"%s\" not \"packed\" in %s",
+ BUG("ref_store is type \"%s\" not \"packed\" in %s",
ref_store->be->name, caller);
refs = (struct packed_ref_store *)ref_store;
if ((refs->store_flags & required_flags) != required_flags)
- die("BUG: unallowed operation (%s), requires %x, has %x\n",
+ BUG("unallowed operation (%s), requires %x, has %x\n",
caller, required_flags, refs->store_flags);
return refs;
"packed_refs_unlock");
if (!is_lock_file_locked(&refs->lock))
- die("BUG: packed_refs_unlock() called when not locked");
+ BUG("packed_refs_unlock() called when not locked");
rollback_lock_file(&refs->lock);
}
char *packed_refs_path;
if (!is_lock_file_locked(&refs->lock))
- die("BUG: write_with_updates() called while unlocked");
+ BUG("write_with_updates() called while unlocked");
/*
* If packed-refs is a symlink, we want to overwrite the
const char *refname, const char *target,
const char *logmsg)
{
- die("BUG: packed reference store does not support symrefs");
+ BUG("packed reference store does not support symrefs");
}
static int packed_rename_ref(struct ref_store *ref_store,
const char *oldrefname, const char *newrefname,
const char *logmsg)
{
- die("BUG: packed reference store does not support renaming references");
+ BUG("packed reference store does not support renaming references");
}
static int packed_copy_ref(struct ref_store *ref_store,
const char *oldrefname, const char *newrefname,
const char *logmsg)
{
- die("BUG: packed reference store does not support copying references");
+ BUG("packed reference store does not support copying references");
}
static struct ref_iterator *packed_reflog_iterator_begin(struct ref_store *ref_store)
const char *refname, int force_create,
struct strbuf *err)
{
- die("BUG: packed reference store does not support reflogs");
+ BUG("packed reference store does not support reflogs");
}
static int packed_delete_reflog(struct ref_store *ref_store,
dir = &entry->u.subdir;
if (entry->flag & REF_INCOMPLETE) {
if (!dir->cache->fill_ref_dir)
- die("BUG: incomplete ref_store without fill_ref_dir function");
+ BUG("incomplete ref_store without fill_ref_dir function");
dir->cache->fill_ref_dir(dir->cache->ref_store, dir, entry->name);
entry->flag &= ~REF_INCOMPLETE;
--- /dev/null
+#include "cache.h"
+#include "argv-array.h"
+#include "refs.h"
+#include "refspec.h"
+
+static struct refspec_item s_tag_refspec = {
+ 0,
+ 1,
+ 0,
+ 0,
+ "refs/tags/*",
+ "refs/tags/*"
+};
+
+/* See TAG_REFSPEC for the string version */
+const struct refspec_item *tag_refspec = &s_tag_refspec;
+
+/*
+ * Parses the provided refspec 'refspec' and populates the refspec_item 'item'.
+ * Returns 1 if successful and 0 if the refspec is invalid.
+ */
+static int parse_refspec(struct refspec_item *item, const char *refspec, int fetch)
+{
+ size_t llen;
+ int is_glob;
+ const char *lhs, *rhs;
+ int flags;
+
+ is_glob = 0;
+
+ lhs = refspec;
+ if (*lhs == '+') {
+ item->force = 1;
+ lhs++;
+ }
+
+ rhs = strrchr(lhs, ':');
+
+ /*
+ * Before going on, special case ":" (or "+:") as a refspec
+ * for pushing matching refs.
+ */
+ if (!fetch && rhs == lhs && rhs[1] == '\0') {
+ item->matching = 1;
+ return 1;
+ }
+
+ if (rhs) {
+ size_t rlen = strlen(++rhs);
+ is_glob = (1 <= rlen && strchr(rhs, '*'));
+ item->dst = xstrndup(rhs, rlen);
+ }
+
+ llen = (rhs ? (rhs - lhs - 1) : strlen(lhs));
+ if (1 <= llen && memchr(lhs, '*', llen)) {
+ if ((rhs && !is_glob) || (!rhs && fetch))
+ return 0;
+ is_glob = 1;
+ } else if (rhs && is_glob) {
+ return 0;
+ }
+
+ item->pattern = is_glob;
+ item->src = xstrndup(lhs, llen);
+ flags = REFNAME_ALLOW_ONELEVEL | (is_glob ? REFNAME_REFSPEC_PATTERN : 0);
+
+ if (fetch) {
+ struct object_id unused;
+
+ /* LHS */
+ if (!*item->src)
+ ; /* empty is ok; it means "HEAD" */
+ else if (llen == GIT_SHA1_HEXSZ && !get_oid_hex(item->src, &unused))
+ item->exact_sha1 = 1; /* ok */
+ else if (!check_refname_format(item->src, flags))
+ ; /* valid looking ref is ok */
+ else
+ return 0;
+ /* RHS */
+ if (!item->dst)
+ ; /* missing is ok; it is the same as empty */
+ else if (!*item->dst)
+ ; /* empty is ok; it means "do not store" */
+ else if (!check_refname_format(item->dst, flags))
+ ; /* valid looking ref is ok */
+ else
+ return 0;
+ } else {
+ /*
+ * LHS
+ * - empty is allowed; it means delete.
+ * - when wildcarded, it must be a valid looking ref.
+ * - otherwise, it must be an extended SHA-1, but
+ * there is no existing way to validate this.
+ */
+ if (!*item->src)
+ ; /* empty is ok */
+ else if (is_glob) {
+ if (check_refname_format(item->src, flags))
+ return 0;
+ }
+ else
+ ; /* anything goes, for now */
+ /*
+ * RHS
+ * - missing is allowed, but LHS then must be a
+ * valid looking ref.
+ * - empty is not allowed.
+ * - otherwise it must be a valid looking ref.
+ */
+ if (!item->dst) {
+ if (check_refname_format(item->src, flags))
+ return 0;
+ } else if (!*item->dst) {
+ return 0;
+ } else {
+ if (check_refname_format(item->dst, flags))
+ return 0;
+ }
+ }
+
+ return 1;
+}
+
+void refspec_item_init(struct refspec_item *item, const char *refspec, int fetch)
+{
+ memset(item, 0, sizeof(*item));
+
+ if (!parse_refspec(item, refspec, fetch))
+ die("Invalid refspec '%s'", refspec);
+}
+
+void refspec_item_clear(struct refspec_item *item)
+{
+ FREE_AND_NULL(item->src);
+ FREE_AND_NULL(item->dst);
+ item->force = 0;
+ item->pattern = 0;
+ item->matching = 0;
+ item->exact_sha1 = 0;
+}
+
+void refspec_init(struct refspec *rs, int fetch)
+{
+ memset(rs, 0, sizeof(*rs));
+ rs->fetch = fetch;
+}
+
+void refspec_append(struct refspec *rs, const char *refspec)
+{
+ struct refspec_item item;
+
+ refspec_item_init(&item, refspec, rs->fetch);
+
+ ALLOC_GROW(rs->items, rs->nr + 1, rs->alloc);
+ rs->items[rs->nr++] = item;
+
+ ALLOC_GROW(rs->raw, rs->raw_nr + 1, rs->raw_alloc);
+ rs->raw[rs->raw_nr++] = xstrdup(refspec);
+}
+
+void refspec_appendn(struct refspec *rs, const char **refspecs, int nr)
+{
+ int i;
+ for (i = 0; i < nr; i++)
+ refspec_append(rs, refspecs[i]);
+}
+
+void refspec_clear(struct refspec *rs)
+{
+ int i;
+
+ for (i = 0; i < rs->nr; i++)
+ refspec_item_clear(&rs->items[i]);
+
+ FREE_AND_NULL(rs->items);
+ rs->alloc = 0;
+ rs->nr = 0;
+
+ for (i = 0; i < rs->raw_nr; i++)
+ free((char *)rs->raw[i]);
+ FREE_AND_NULL(rs->raw);
+ rs->raw_alloc = 0;
+ rs->raw_nr = 0;
+
+ rs->fetch = 0;
+}
+
+int valid_fetch_refspec(const char *fetch_refspec_str)
+{
+ struct refspec_item refspec;
+ int ret = parse_refspec(&refspec, fetch_refspec_str, REFSPEC_FETCH);
+ refspec_item_clear(&refspec);
+ return ret;
+}
+
+void refspec_ref_prefixes(const struct refspec *rs,
+ struct argv_array *ref_prefixes)
+{
+ int i;
+ for (i = 0; i < rs->nr; i++) {
+ const struct refspec_item *item = &rs->items[i];
+ const char *prefix = NULL;
+
+ if (rs->fetch == REFSPEC_FETCH)
+ prefix = item->src;
+ else if (item->dst)
+ prefix = item->dst;
+ else if (item->src && !item->exact_sha1)
+ prefix = item->src;
+
+ if (prefix) {
+ if (item->pattern) {
+ const char *glob = strchr(prefix, '*');
+ argv_array_pushf(ref_prefixes, "%.*s",
+ (int)(glob - prefix),
+ prefix);
+ } else {
+ expand_ref_prefix(ref_prefixes, prefix);
+ }
+ }
+ }
+}
--- /dev/null
+#ifndef REFSPEC_H
+#define REFSPEC_H
+
+#define TAG_REFSPEC "refs/tags/*:refs/tags/*"
+extern const struct refspec_item *tag_refspec;
+
+struct refspec_item {
+ unsigned force : 1;
+ unsigned pattern : 1;
+ unsigned matching : 1;
+ unsigned exact_sha1 : 1;
+
+ char *src;
+ char *dst;
+};
+
+#define REFSPEC_FETCH 1
+#define REFSPEC_PUSH 0
+
+#define REFSPEC_INIT_FETCH { .fetch = REFSPEC_FETCH }
+#define REFSPEC_INIT_PUSH { .fetch = REFSPEC_PUSH }
+
+struct refspec {
+ struct refspec_item *items;
+ int alloc;
+ int nr;
+
+ const char **raw;
+ int raw_alloc;
+ int raw_nr;
+
+ int fetch;
+};
+
+void refspec_item_init(struct refspec_item *item, const char *refspec, int fetch);
+void refspec_item_clear(struct refspec_item *item);
+void refspec_init(struct refspec *rs, int fetch);
+void refspec_append(struct refspec *rs, const char *refspec);
+void refspec_appendn(struct refspec *rs, const char **refspecs, int nr);
+void refspec_clear(struct refspec *rs);
+
+int valid_fetch_refspec(const char *refspec);
+
+struct argv_array;
+void refspec_ref_prefixes(const struct refspec *rs,
+ struct argv_array *ref_prefixes);
+
+#endif /* REFSPEC_H */
#include "config.h"
#include "remote.h"
#include "refs.h"
+#include "refspec.h"
#include "commit.h"
#include "diff.h"
#include "revision.h"
enum map_direction { FROM_SRC, FROM_DST };
-static struct refspec s_tag_refspec = {
- 0,
- 1,
- 0,
- 0,
- "refs/tags/*",
- "refs/tags/*"
-};
-
-/* See TAG_REFSPEC for the string version */
-const struct refspec *tag_refspec = &s_tag_refspec;
-
struct counted_string {
size_t len;
const char *s;
return xstrfmt("%s%s", r->rewrite[longest_i]->base, url + longest->len);
}
-static void add_push_refspec(struct remote *remote, const char *ref)
-{
- ALLOC_GROW(remote->push_refspec,
- remote->push_refspec_nr + 1,
- remote->push_refspec_alloc);
- remote->push_refspec[remote->push_refspec_nr++] = ref;
-}
-
-static void add_fetch_refspec(struct remote *remote, const char *ref)
-{
- ALLOC_GROW(remote->fetch_refspec,
- remote->fetch_refspec_nr + 1,
- remote->fetch_refspec_alloc);
- remote->fetch_refspec[remote->fetch_refspec_nr++] = ref;
-}
-
-void add_prune_tags_to_fetch_refspec(struct remote *remote)
-{
- int nr = remote->fetch_refspec_nr;
- int bufsize = nr + 1;
- int size = sizeof(struct refspec);
-
- remote->fetch = xrealloc(remote->fetch, size * bufsize);
- memcpy(&remote->fetch[nr], tag_refspec, size);
- add_fetch_refspec(remote, xstrdup(TAG_REFSPEC));
-}
-
static void add_url(struct remote *remote, const char *url)
{
ALLOC_GROW(remote->url, remote->url_nr + 1, remote->url_alloc);
ret = xcalloc(1, sizeof(struct remote));
ret->prune = -1; /* unspecified */
ret->prune_tags = -1; /* unspecified */
+ ret->name = xstrndup(name, len);
+ refspec_init(&ret->push, REFSPEC_PUSH);
+ refspec_init(&ret->fetch, REFSPEC_FETCH);
+
ALLOC_GROW(remotes, remotes_nr + 1, remotes_alloc);
remotes[remotes_nr++] = ret;
- ret->name = xstrndup(name, len);
hashmap_entry_init(ret, lookup_entry.hash);
replaced = hashmap_put(&remotes_hash, ret);
if (skip_prefix(buf.buf, "URL:", &v))
add_url_alias(remote, xstrdup(skip_spaces(v)));
else if (skip_prefix(buf.buf, "Push:", &v))
- add_push_refspec(remote, xstrdup(skip_spaces(v)));
+ refspec_append(&remote->push, skip_spaces(v));
else if (skip_prefix(buf.buf, "Pull:", &v))
- add_fetch_refspec(remote, xstrdup(skip_spaces(v)));
+ refspec_append(&remote->fetch, skip_spaces(v));
}
strbuf_release(&buf);
fclose(f);
frag = "master";
add_url_alias(remote, strbuf_detach(&buf, NULL));
- add_fetch_refspec(remote, xstrfmt("refs/heads/%s:refs/heads/%s",
- frag, remote->name));
+ strbuf_addf(&buf, "refs/heads/%s:refs/heads/%s",
+ frag, remote->name);
+ refspec_append(&remote->fetch, buf.buf);
/*
* Cogito compatible push: push current HEAD to remote #branch
* (master if missing)
*/
- add_push_refspec(remote, xstrfmt("HEAD:refs/heads/%s", frag));
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "HEAD:refs/heads/%s", frag);
+ refspec_append(&remote->push, buf.buf);
remote->fetch_tags = 1; /* always auto-follow */
+ strbuf_release(&buf);
}
static int handle_config(const char *key, const char *value, void *cb)
const char *v;
if (git_config_string(&v, key, value))
return -1;
- add_push_refspec(remote, v);
+ refspec_append(&remote->push, v);
+ free((char *)v);
} else if (!strcmp(subkey, "fetch")) {
const char *v;
if (git_config_string(&v, key, value))
return -1;
- add_fetch_refspec(remote, v);
+ refspec_append(&remote->fetch, v);
+ free((char *)v);
} else if (!strcmp(subkey, "receivepack")) {
const char *v;
if (git_config_string(&v, key, value))
alias_all_urls();
}
-static struct refspec *parse_refspec_internal(int nr_refspec, const char **refspec, int fetch, int verify)
-{
- int i;
- struct refspec *rs = xcalloc(nr_refspec, sizeof(*rs));
-
- for (i = 0; i < nr_refspec; i++) {
- size_t llen;
- int is_glob;
- const char *lhs, *rhs;
- int flags;
-
- is_glob = 0;
-
- lhs = refspec[i];
- if (*lhs == '+') {
- rs[i].force = 1;
- lhs++;
- }
-
- rhs = strrchr(lhs, ':');
-
- /*
- * Before going on, special case ":" (or "+:") as a refspec
- * for pushing matching refs.
- */
- if (!fetch && rhs == lhs && rhs[1] == '\0') {
- rs[i].matching = 1;
- continue;
- }
-
- if (rhs) {
- size_t rlen = strlen(++rhs);
- is_glob = (1 <= rlen && strchr(rhs, '*'));
- rs[i].dst = xstrndup(rhs, rlen);
- }
-
- llen = (rhs ? (rhs - lhs - 1) : strlen(lhs));
- if (1 <= llen && memchr(lhs, '*', llen)) {
- if ((rhs && !is_glob) || (!rhs && fetch))
- goto invalid;
- is_glob = 1;
- } else if (rhs && is_glob) {
- goto invalid;
- }
-
- rs[i].pattern = is_glob;
- rs[i].src = xstrndup(lhs, llen);
- flags = REFNAME_ALLOW_ONELEVEL | (is_glob ? REFNAME_REFSPEC_PATTERN : 0);
-
- if (fetch) {
- struct object_id unused;
-
- /* LHS */
- if (!*rs[i].src)
- ; /* empty is ok; it means "HEAD" */
- else if (llen == GIT_SHA1_HEXSZ && !get_oid_hex(rs[i].src, &unused))
- rs[i].exact_sha1 = 1; /* ok */
- else if (!check_refname_format(rs[i].src, flags))
- ; /* valid looking ref is ok */
- else
- goto invalid;
- /* RHS */
- if (!rs[i].dst)
- ; /* missing is ok; it is the same as empty */
- else if (!*rs[i].dst)
- ; /* empty is ok; it means "do not store" */
- else if (!check_refname_format(rs[i].dst, flags))
- ; /* valid looking ref is ok */
- else
- goto invalid;
- } else {
- /*
- * LHS
- * - empty is allowed; it means delete.
- * - when wildcarded, it must be a valid looking ref.
- * - otherwise, it must be an extended SHA-1, but
- * there is no existing way to validate this.
- */
- if (!*rs[i].src)
- ; /* empty is ok */
- else if (is_glob) {
- if (check_refname_format(rs[i].src, flags))
- goto invalid;
- }
- else
- ; /* anything goes, for now */
- /*
- * RHS
- * - missing is allowed, but LHS then must be a
- * valid looking ref.
- * - empty is not allowed.
- * - otherwise it must be a valid looking ref.
- */
- if (!rs[i].dst) {
- if (check_refname_format(rs[i].src, flags))
- goto invalid;
- } else if (!*rs[i].dst) {
- goto invalid;
- } else {
- if (check_refname_format(rs[i].dst, flags))
- goto invalid;
- }
- }
- }
- return rs;
-
- invalid:
- if (verify) {
- /*
- * nr_refspec must be greater than zero and i must be valid
- * since it is only possible to reach this point from within
- * the for loop above.
- */
- free_refspec(i+1, rs);
- return NULL;
- }
- die("Invalid refspec '%s'", refspec[i]);
-}
-
-int valid_fetch_refspec(const char *fetch_refspec_str)
-{
- struct refspec *refspec;
-
- refspec = parse_refspec_internal(1, &fetch_refspec_str, 1, 1);
- free_refspec(1, refspec);
- return !!refspec;
-}
-
-struct refspec *parse_fetch_refspec(int nr_refspec, const char **refspec)
-{
- return parse_refspec_internal(nr_refspec, refspec, 1, 0);
-}
-
-struct refspec *parse_push_refspec(int nr_refspec, const char **refspec)
-{
- return parse_refspec_internal(nr_refspec, refspec, 0, 0);
-}
-
-void free_refspec(int nr_refspec, struct refspec *refspec)
-{
- int i;
-
- if (!refspec)
- return;
-
- for (i = 0; i < nr_refspec; i++) {
- free(refspec[i].src);
- free(refspec[i].dst);
- }
- free(refspec);
-}
-
static int valid_remote_nick(const char *name)
{
if (!name[0] || is_dot_or_dotdot(name))
pushremote_for_branch(branch, NULL);
struct remote *remote = remote_get(remote_name);
- if (remote && remote->push_refspec_nr &&
- (dst = apply_refspecs(remote->push,
- remote->push_refspec_nr,
+ if (remote && remote->push.nr &&
+ (dst = apply_refspecs(&remote->push,
branch->refname))) {
if (explicit)
*explicit = 1;
add_url_alias(ret, name);
if (!valid_remote(ret))
return NULL;
- ret->fetch = parse_fetch_refspec(ret->fetch_refspec_nr, ret->fetch_refspec);
- ret->push = parse_push_refspec(ret->push_refspec_nr, ret->push_refspec);
return ret;
}
struct remote *r = remotes[i];
if (!r)
continue;
- if (!r->fetch)
- r->fetch = parse_fetch_refspec(r->fetch_refspec_nr,
- r->fetch_refspec);
- if (!r->push)
- r->push = parse_push_refspec(r->push_refspec_nr,
- r->push_refspec);
result = fn(r, priv);
}
return result;
return ret;
}
-static void query_refspecs_multiple(struct refspec *refs, int ref_count, struct refspec *query, struct string_list *results)
+static void query_refspecs_multiple(struct refspec *rs,
+ struct refspec_item *query,
+ struct string_list *results)
{
int i;
int find_src = !query->src;
if (find_src && !query->dst)
error("query_refspecs_multiple: need either src or dst");
- for (i = 0; i < ref_count; i++) {
- struct refspec *refspec = &refs[i];
+ for (i = 0; i < rs->nr; i++) {
+ struct refspec_item *refspec = &rs->items[i];
const char *key = find_src ? refspec->dst : refspec->src;
const char *value = find_src ? refspec->src : refspec->dst;
const char *needle = find_src ? query->dst : query->src;
}
}
-int query_refspecs(struct refspec *refs, int ref_count, struct refspec *query)
+int query_refspecs(struct refspec *rs, struct refspec_item *query)
{
int i;
int find_src = !query->src;
if (find_src && !query->dst)
return error("query_refspecs: need either src or dst");
- for (i = 0; i < ref_count; i++) {
- struct refspec *refspec = &refs[i];
+ for (i = 0; i < rs->nr; i++) {
+ struct refspec_item *refspec = &rs->items[i];
const char *key = find_src ? refspec->dst : refspec->src;
const char *value = find_src ? refspec->src : refspec->dst;
return -1;
}
-char *apply_refspecs(struct refspec *refspecs, int nr_refspec,
- const char *name)
+char *apply_refspecs(struct refspec *rs, const char *name)
{
- struct refspec query;
+ struct refspec_item query;
- memset(&query, 0, sizeof(struct refspec));
+ memset(&query, 0, sizeof(struct refspec_item));
query.src = (char *)name;
- if (query_refspecs(refspecs, nr_refspec, &query))
+ if (query_refspecs(rs, &query))
return NULL;
return query.dst;
}
-int remote_find_tracking(struct remote *remote, struct refspec *refspec)
+int remote_find_tracking(struct remote *remote, struct refspec_item *refspec)
{
- return query_refspecs(remote->fetch, remote->fetch_refspec_nr, refspec);
+ return query_refspecs(&remote->fetch, refspec);
}
static struct ref *alloc_ref_with_prefix(const char *prefix, size_t prefixlen,
}
static int match_explicit_lhs(struct ref *src,
- struct refspec *rs,
+ struct refspec_item *rs,
struct ref **match,
int *allocated_match)
{
static int match_explicit(struct ref *src, struct ref *dst,
struct ref ***dst_tail,
- struct refspec *rs)
+ struct refspec_item *rs)
{
struct ref *matched_src, *matched_dst;
int allocated_src;
}
static int match_explicit_refs(struct ref *src, struct ref *dst,
- struct ref ***dst_tail, struct refspec *rs,
- int rs_nr)
+ struct ref ***dst_tail, struct refspec *rs)
{
int i, errs;
- for (i = errs = 0; i < rs_nr; i++)
- errs += match_explicit(src, dst, dst_tail, &rs[i]);
+ for (i = errs = 0; i < rs->nr; i++)
+ errs += match_explicit(src, dst, dst_tail, &rs->items[i]);
return errs;
}
-static char *get_ref_match(const struct refspec *rs, int rs_nr, const struct ref *ref,
- int send_mirror, int direction, const struct refspec **ret_pat)
+static char *get_ref_match(const struct refspec *rs, const struct ref *ref,
+ int send_mirror, int direction,
+ const struct refspec_item **ret_pat)
{
- const struct refspec *pat;
+ const struct refspec_item *pat;
char *name;
int i;
int matching_refs = -1;
- for (i = 0; i < rs_nr; i++) {
- if (rs[i].matching &&
- (matching_refs == -1 || rs[i].force)) {
+ for (i = 0; i < rs->nr; i++) {
+ const struct refspec_item *item = &rs->items[i];
+ if (item->matching &&
+ (matching_refs == -1 || item->force)) {
matching_refs = i;
continue;
}
- if (rs[i].pattern) {
- const char *dst_side = rs[i].dst ? rs[i].dst : rs[i].src;
+ if (item->pattern) {
+ const char *dst_side = item->dst ? item->dst : item->src;
int match;
if (direction == FROM_SRC)
- match = match_name_with_pattern(rs[i].src, ref->name, dst_side, &name);
+ match = match_name_with_pattern(item->src, ref->name, dst_side, &name);
else
- match = match_name_with_pattern(dst_side, ref->name, rs[i].src, &name);
+ match = match_name_with_pattern(dst_side, ref->name, item->src, &name);
if (match) {
matching_refs = i;
break;
if (matching_refs == -1)
return NULL;
- pat = rs + matching_refs;
+ pat = &rs->items[matching_refs];
if (pat->matching) {
/*
* "matching refs"; traditionally we pushed everything
continue; /* not a tag */
if (string_list_has_string(&dst_tag, ref->name))
continue; /* they already have it */
- if (oid_object_info(&ref->new_oid, NULL) != OBJ_TAG)
+ if (oid_object_info(the_repository, &ref->new_oid, NULL) != OBJ_TAG)
continue; /* be conservative */
item = string_list_append(&src_tag, ref->name);
item->util = ref;
* but we can catch some errors early before even talking to the
* remote side.
*/
-int check_push_refs(struct ref *src, int nr_refspec, const char **refspec_names)
+int check_push_refs(struct ref *src, struct refspec *rs)
{
- struct refspec *refspec = parse_push_refspec(nr_refspec, refspec_names);
int ret = 0;
int i;
- for (i = 0; i < nr_refspec; i++) {
- struct refspec *rs = refspec + i;
+ for (i = 0; i < rs->nr; i++) {
+ struct refspec_item *item = &rs->items[i];
- if (rs->pattern || rs->matching)
+ if (item->pattern || item->matching)
continue;
- ret |= match_explicit_lhs(src, rs, NULL, NULL);
+ ret |= match_explicit_lhs(src, item, NULL, NULL);
}
- free_refspec(nr_refspec, refspec);
return ret;
}
* dst (e.g. pushing to a new branch, done in match_explicit_refs).
*/
int match_push_refs(struct ref *src, struct ref **dst,
- int nr_refspec, const char **refspec, int flags)
+ struct refspec *rs, int flags)
{
- struct refspec *rs;
int send_all = flags & MATCH_REFS_ALL;
int send_mirror = flags & MATCH_REFS_MIRROR;
int send_prune = flags & MATCH_REFS_PRUNE;
int errs;
- static const char *default_refspec[] = { ":", NULL };
struct ref *ref, **dst_tail = tail_ref(dst);
struct string_list dst_ref_index = STRING_LIST_INIT_NODUP;
- if (!nr_refspec) {
- nr_refspec = 1;
- refspec = default_refspec;
- }
- rs = parse_push_refspec(nr_refspec, (const char **) refspec);
- errs = match_explicit_refs(src, *dst, &dst_tail, rs, nr_refspec);
+ /* If no refspec is provided, use the default ":" */
+ if (!rs->nr)
+ refspec_append(rs, ":");
+
+ errs = match_explicit_refs(src, *dst, &dst_tail, rs);
/* pick the remainder */
for (ref = src; ref; ref = ref->next) {
struct string_list_item *dst_item;
struct ref *dst_peer;
- const struct refspec *pat = NULL;
+ const struct refspec_item *pat = NULL;
char *dst_name;
- dst_name = get_ref_match(rs, nr_refspec, ref, send_mirror, FROM_SRC, &pat);
+ dst_name = get_ref_match(rs, ref, send_mirror, FROM_SRC, &pat);
if (!dst_name)
continue;
/* We're already sending something to this ref. */
continue;
- src_name = get_ref_match(rs, nr_refspec, ref, send_mirror, FROM_DST, NULL);
+ src_name = get_ref_match(rs, ref, send_mirror, FROM_DST, NULL);
if (src_name) {
if (!src_ref_index.nr)
prepare_ref_index(&src_ref_index, src);
}
string_list_clear(&src_ref_index, 0);
}
+
if (errs)
return -1;
return 0;
{
char *ret;
- ret = apply_refspecs(remote->fetch, remote->fetch_refspec_nr, refname);
+ ret = apply_refspecs(&remote->fetch, refname);
if (!ret)
return error_buf(err,
_("push destination '%s' on remote '%s' has no local tracking branch"),
_("branch '%s' has no remote for pushing"),
branch->name);
- if (remote->push_refspec_nr) {
+ if (remote->push.nr) {
char *dst;
const char *ret;
- dst = apply_refspecs(remote->push, remote->push_refspec_nr,
- branch->refname);
+ dst = apply_refspecs(&remote->push, branch->refname);
if (!dst)
return error_buf(err,
_("push refspecs for '%s' do not include '%s'"),
}
}
- die("BUG: unhandled push situation");
+ BUG("unhandled push situation");
}
const char *branch_get_push(struct branch *branch, struct strbuf *err)
* local symbolic ref.
*/
static struct ref *get_expanded_map(const struct ref *remote_refs,
- const struct refspec *refspec)
+ const struct refspec_item *refspec)
{
const struct ref *ref;
struct ref *ret = NULL;
}
int get_fetch_map(const struct ref *remote_refs,
- const struct refspec *refspec,
+ const struct refspec_item *refspec,
struct ref ***tail,
int missing_ok)
{
struct stale_heads_info {
struct string_list *ref_names;
struct ref **stale_refs_tail;
- struct refspec *refs;
- int ref_count;
+ struct refspec *rs;
};
static int get_stale_heads_cb(const char *refname, const struct object_id *oid,
{
struct stale_heads_info *info = cb_data;
struct string_list matches = STRING_LIST_INIT_DUP;
- struct refspec query;
+ struct refspec_item query;
int i, stale = 1;
- memset(&query, 0, sizeof(struct refspec));
+ memset(&query, 0, sizeof(struct refspec_item));
query.dst = (char *)refname;
- query_refspecs_multiple(info->refs, info->ref_count, &query, &matches);
+ query_refspecs_multiple(info->rs, &query, &matches);
if (matches.nr == 0)
goto clean_exit; /* No matches */
return 0;
}
-struct ref *get_stale_heads(struct refspec *refs, int ref_count, struct ref *fetch_map)
+struct ref *get_stale_heads(struct refspec *rs, struct ref *fetch_map)
{
struct ref *ref, *stale_refs = NULL;
struct string_list ref_names = STRING_LIST_INIT_NODUP;
info.ref_names = &ref_names;
info.stale_refs_tail = &stale_refs;
- info.refs = refs;
- info.ref_count = ref_count;
+ info.rs = rs;
for (ref = fetch_map; ref; ref = ref->next)
string_list_append(&ref_names, ref->name);
string_list_sort(&ref_names);
{
char *dst;
- dst = apply_refspecs(remote->fetch, remote->fetch_refspec_nr, refname);
+ dst = apply_refspecs(&remote->fetch, refname);
if (!dst)
return -1; /* no tracking ref for refname at remote */
if (read_ref(dst, oid))
#include "parse-options.h"
#include "hashmap.h"
+#include "refspec.h"
enum {
REMOTE_UNCONFIGURED = 0,
int pushurl_nr;
int pushurl_alloc;
- const char **push_refspec;
- struct refspec *push;
- int push_refspec_nr;
- int push_refspec_alloc;
+ struct refspec push;
- const char **fetch_refspec;
- struct refspec *fetch;
- int fetch_refspec_nr;
- int fetch_refspec_alloc;
+ struct refspec fetch;
/*
* -1 to never fetch tags
int remote_has_url(struct remote *remote, const char *url);
-struct refspec {
- unsigned force : 1;
- unsigned pattern : 1;
- unsigned matching : 1;
- unsigned exact_sha1 : 1;
-
- char *src;
- char *dst;
-};
-
-extern const struct refspec *tag_refspec;
-
struct ref {
struct ref *next;
struct object_id old_oid;
struct oid_array;
struct packet_reader;
struct argv_array;
+struct string_list;
extern struct ref **get_remote_heads(struct packet_reader *reader,
struct ref **list, unsigned int flags,
struct oid_array *extra_have,
/* Used for protocol v2 in order to retrieve refs from a remote */
extern struct ref **get_remote_refs(int fd_out, struct packet_reader *reader,
struct ref **list, int for_push,
- const struct argv_array *ref_prefixes);
+ const struct argv_array *ref_prefixes,
+ const struct string_list *server_options);
int resolve_remote_symref(struct ref *ref, struct ref *list);
int ref_newer(const struct object_id *new_oid, const struct object_id *old_oid);
*/
struct ref *ref_remove_duplicates(struct ref *ref_map);
-int valid_fetch_refspec(const char *refspec);
-struct refspec *parse_fetch_refspec(int nr_refspec, const char **refspec);
-extern struct refspec *parse_push_refspec(int nr_refspec, const char **refspec);
+int query_refspecs(struct refspec *rs, struct refspec_item *query);
+char *apply_refspecs(struct refspec *rs, const char *name);
-void free_refspec(int nr_refspec, struct refspec *refspec);
-
-extern int query_refspecs(struct refspec *specs, int nr, struct refspec *query);
-char *apply_refspecs(struct refspec *refspecs, int nr_refspec,
- const char *name);
-
-int check_push_refs(struct ref *src, int nr_refspec, const char **refspec);
+int check_push_refs(struct ref *src, struct refspec *rs);
int match_push_refs(struct ref *src, struct ref **dst,
- int nr_refspec, const char **refspec, int all);
+ struct refspec *rs, int flags);
void set_ref_status_for_push(struct ref *remote_refs, int send_mirror,
int force_update);
* missing_ok is usually false, but when we are adding branch.$name.merge
* it is Ok if the branch is not at the remote anymore.
*/
-int get_fetch_map(const struct ref *remote_refs, const struct refspec *refspec,
+int get_fetch_map(const struct ref *remote_refs, const struct refspec_item *refspec,
struct ref ***tail, int missing_ok);
struct ref *get_remote_ref(const struct ref *remote_refs, const char *name);
/*
* For the given remote, reads the refspec's src and sets the other fields.
*/
-int remote_find_tracking(struct remote *remote, struct refspec *refspec);
+int remote_find_tracking(struct remote *remote, struct refspec_item *refspec);
struct branch {
const char *name;
const char *pushremote_name;
const char **merge_name;
- struct refspec **merge;
+ struct refspec_item **merge;
int merge_nr;
int merge_alloc;
int all);
/* Return refs which no longer exist on remote */
-struct ref *get_stale_heads(struct refspec *refs, int ref_count, struct ref *fetch_map);
+struct ref *get_stale_heads(struct refspec *rs, struct ref *fetch_map);
/*
* Compare-and-swap
extern int is_empty_cas(const struct push_cas_option *);
void apply_push_cas(struct push_cas_option *, struct remote *, struct ref *);
-#define TAG_REFSPEC "refs/tags/*:refs/tags/*"
-
-void add_prune_tags_to_fetch_refspec(struct remote *remote);
-
#endif
return;
r->objects->replace_map =
- xmalloc(sizeof(*the_repository->objects->replace_map));
+ xmalloc(sizeof(*r->objects->replace_map));
oidmap_init(r->objects->replace_map, 0);
for_each_replace_ref(r, register_replace_ref, NULL);
if (repo->index) {
discard_index(repo->index);
- FREE_AND_NULL(repo->index);
+ if (repo->index != &the_index)
+ FREE_AND_NULL(repo->index);
}
}
return ret;
}
-static struct lock_file index_lock;
-
static void update_paths(struct string_list *update)
{
+ struct lock_file index_lock = LOCK_INIT;
int i;
hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR);
continue;
if (size < rawsz)
goto error;
- memcpy(ui->oid[i].hash, (const unsigned char *)data, rawsz);
+ oidread(&ui->oid[i], (const unsigned char *)data);
size -= rawsz;
data += rawsz;
}
{
struct tree_desc desc;
struct name_entry entry;
- struct object *obj = &tree->object;
- if (!has_object_file(&obj->oid))
+ if (parse_tree_gently(tree, 1) < 0)
return;
- if (parse_tree(tree) < 0)
- die("bad tree %s", oid_to_hex(&obj->oid));
init_tree_desc(&desc, tree->buffer, tree->size);
while (tree_entry(&desc, &entry)) {
mark_tree_contents_uninteresting(tree);
}
-void mark_parents_uninteresting(struct commit *commit)
+struct commit_stack {
+ struct commit **items;
+ size_t nr, alloc;
+};
+#define COMMIT_STACK_INIT { NULL, 0, 0 }
+
+static void commit_stack_push(struct commit_stack *stack, struct commit *commit)
{
- struct commit_list *parents = NULL, *l;
+ ALLOC_GROW(stack->items, stack->nr + 1, stack->alloc);
+ stack->items[stack->nr++] = commit;
+}
- for (l = commit->parents; l; l = l->next)
- commit_list_insert(l->item, &parents);
+static struct commit *commit_stack_pop(struct commit_stack *stack)
+{
+ return stack->nr ? stack->items[--stack->nr] : NULL;
+}
- while (parents) {
- struct commit *commit = pop_commit(&parents);
+static void commit_stack_clear(struct commit_stack *stack)
+{
+ FREE_AND_NULL(stack->items);
+ stack->nr = stack->alloc = 0;
+}
- while (commit) {
- /*
- * A missing commit is ok iff its parent is marked
- * uninteresting.
- *
- * We just mark such a thing parsed, so that when
- * it is popped next time around, we won't be trying
- * to parse it and get an error.
- */
- if (!commit->object.parsed &&
- !has_object_file(&commit->object.oid))
- commit->object.parsed = 1;
+static void mark_one_parent_uninteresting(struct commit *commit,
+ struct commit_stack *pending)
+{
+ struct commit_list *l;
- if (commit->object.flags & UNINTERESTING)
- break;
+ if (commit->object.flags & UNINTERESTING)
+ return;
+ commit->object.flags |= UNINTERESTING;
+
+ /*
+ * Normally we haven't parsed the parent
+ * yet, so we won't have a parent of a parent
+ * here. However, it may turn out that we've
+ * reached this commit some other way (where it
+ * wasn't uninteresting), in which case we need
+ * to mark its parents recursively too..
+ */
+ for (l = commit->parents; l; l = l->next)
+ commit_stack_push(pending, l->item);
+}
- commit->object.flags |= UNINTERESTING;
+void mark_parents_uninteresting(struct commit *commit)
+{
+ struct commit_stack pending = COMMIT_STACK_INIT;
+ struct commit_list *l;
- /*
- * Normally we haven't parsed the parent
- * yet, so we won't have a parent of a parent
- * here. However, it may turn out that we've
- * reached this commit some other way (where it
- * wasn't uninteresting), in which case we need
- * to mark its parents recursively too..
- */
- if (!commit->parents)
- break;
+ for (l = commit->parents; l; l = l->next)
+ mark_one_parent_uninteresting(l->item, &pending);
- for (l = commit->parents->next; l; l = l->next)
- commit_list_insert(l->item, &parents);
- commit = commit->parents->item;
- }
- }
+ while (pending.nr > 0)
+ mark_one_parent_uninteresting(commit_stack_pop(&pending),
+ &pending);
+
+ commit_stack_clear(&pending);
}
static void add_pending_object_with_path(struct rev_info *revs,
static int rev_compare_tree(struct rev_info *revs,
struct commit *parent, struct commit *commit)
{
- struct tree *t1 = parent->tree;
- struct tree *t2 = commit->tree;
+ struct tree *t1 = get_commit_tree(parent);
+ struct tree *t2 = get_commit_tree(commit);
if (!t1)
return REV_TREE_NEW;
static int rev_same_tree_as_empty(struct rev_info *revs, struct commit *commit)
{
int retval;
- struct tree *t1 = commit->tree;
+ struct tree *t1 = get_commit_tree(commit);
if (!t1)
return 0;
if (!revs->prune)
return;
- if (!commit->tree)
+ if (!get_commit_tree(commit))
return;
if (!commit->parents) {
const char *arg = argv[0];
const char *optarg;
int argcount;
+ const unsigned hexsz = the_hash_algo->hexsz;
/* pseudo revision arguments */
if (!strcmp(arg, "--all") || !strcmp(arg, "--branches") ||
revs->abbrev = strtoul(optarg, NULL, 10);
if (revs->abbrev < MINIMUM_ABBREV)
revs->abbrev = MINIMUM_ABBREV;
- else if (revs->abbrev > 40)
- revs->abbrev = 40;
+ else if (revs->abbrev > hexsz)
+ revs->abbrev = hexsz;
} else if (!strcmp(arg, "--abbrev-commit")) {
revs->abbrev_commit = 1;
revs->abbrev_commit_given = 1;
revs->ignore_missing = 1;
} else if (!strcmp(arg, "--exclude-promisor-objects")) {
if (fetch_if_missing)
- die("BUG: exclude_promisor_objects can only be used when fetch_if_missing is 0");
+ BUG("exclude_promisor_objects can only be used when fetch_if_missing is 0");
revs->exclude_promisor_objects = 1;
} else {
int opts = diff_opt_parse(&revs->diffopt, argv, argc, revs->prefix);
* supported right now, so stick to single worktree.
*/
if (!revs->single_worktree)
- die("BUG: --single-worktree cannot be used together with submodule");
+ BUG("--single-worktree cannot be used together with submodule");
refs = get_submodule_ref_store(submodule);
} else
refs = get_main_ref_store(the_repository);
{
if (commit->object.flags & SHOWN)
return commit_ignore;
- if (revs->unpacked && has_sha1_pack(commit->object.oid.hash))
+ if (revs->unpacked && has_object_pack(&commit->object.oid))
return commit_ignore;
if (commit->object.flags & UNINTERESTING)
return commit_ignore;
static const char **prepare_shell_cmd(struct argv_array *out, const char **argv)
{
if (!argv[0])
- die("BUG: shell command is empty");
+ BUG("shell command is empty");
if (strcspn(argv[0], "|&;<>()$`\\\"' \t\n*?[#~=%") != strlen(argv[0])) {
#ifndef GIT_WINDOWS_NATIVE
static void prepare_cmd(struct argv_array *out, const struct child_process *cmd)
{
if (!cmd->argv[0])
- die("BUG: command is empty");
+ BUG("command is empty");
/*
* Add SHELL_PATH so in the event exec fails with ENOEXEC we can
sigset_t old;
};
-#ifndef NO_PTHREADS
-static void bug_die(int err, const char *msg)
-{
- if (err) {
- errno = err;
- die_errno("BUG: %s", msg);
- }
-}
-#endif
+#define CHECK_BUG(err, msg) \
+ do { \
+ int e = (err); \
+ if (e) \
+ BUG("%s: %s", msg, strerror(e)); \
+ } while(0)
static void atfork_prepare(struct atfork_state *as)
{
if (sigprocmask(SIG_SETMASK, &all, &as->old))
die_errno("sigprocmask");
#else
- bug_die(pthread_sigmask(SIG_SETMASK, &all, &as->old),
+ CHECK_BUG(pthread_sigmask(SIG_SETMASK, &all, &as->old),
"blocking all signals");
- bug_die(pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &as->cs),
+ CHECK_BUG(pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &as->cs),
"disabling cancellation");
#endif
}
if (sigprocmask(SIG_SETMASK, &as->old, NULL))
die_errno("sigprocmask");
#else
- bug_die(pthread_setcancelstate(as->cs, NULL),
+ CHECK_BUG(pthread_setcancelstate(as->cs, NULL),
"re-enabling cancellation");
- bug_die(pthread_sigmask(SIG_SETMASK, &as->old, NULL),
+ CHECK_BUG(pthread_sigmask(SIG_SETMASK, &as->old, NULL),
"restoring signal mask");
#endif
}
int code;
if (cmd->out < 0 || cmd->err < 0)
- die("BUG: run_command with a pipe can cause deadlock");
+ BUG("run_command with a pipe can cause deadlock");
code = start_command(cmd);
if (code)
pp->data = data;
if (!get_next_task)
- die("BUG: you need to specify a get_next_task function");
+ BUG("you need to specify a get_next_task function");
pp->get_next_task = get_next_task;
pp->start_failure = start_failure ? start_failure : default_start_failure;
if (pp->children[i].state == GIT_CP_FREE)
break;
if (i == pp->max_processes)
- die("BUG: bookkeeping is hard");
+ BUG("bookkeeping is hard");
code = pp->get_next_task(&pp->children[i].process,
&pp->children[i].err,
#include "hashmap.h"
#include "notes-utils.h"
#include "sigchain.h"
+#include "unpack-trees.h"
+#include "worktree.h"
+#include "oidmap.h"
+#include "oidset.h"
#define GIT_REFLOG_ACTION "GIT_REFLOG_ACTION"
* previous commit and from the first squash/fixup commit are written
* to it. The commit message for each subsequent squash/fixup commit
* is appended to the file as it is processed.
- *
- * The first line of the file is of the form
- * # This is a combination of $count commits.
- * where $count is the number of commits whose messages have been
- * written to the file so far (including the initial "pick" commit).
- * Each time that a commit message is processed, this line is read and
- * updated. It is deleted just before the combined commit is made.
*/
static GIT_PATH_FUNC(rebase_path_squash_msg, "rebase-merge/message-squash")
/*
* commit without opening the editor.)
*/
static GIT_PATH_FUNC(rebase_path_fixup_msg, "rebase-merge/message-fixup")
+/*
+ * This file contains the list fixup/squash commands that have been
+ * accumulated into message-fixup or message-squash so far.
+ */
+static GIT_PATH_FUNC(rebase_path_current_fixups, "rebase-merge/current-fixups")
/*
* A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
* GIT_AUTHOR_DATE that will be used for the commit that is currently
static GIT_PATH_FUNC(rebase_path_rewritten_list, "rebase-merge/rewritten-list")
static GIT_PATH_FUNC(rebase_path_rewritten_pending,
"rebase-merge/rewritten-pending")
+
+/*
+ * The path of the file containig the OID of the "squash onto" commit, i.e.
+ * the dummy commit used for `reset [new root]`.
+ */
+static GIT_PATH_FUNC(rebase_path_squash_onto, "rebase-merge/squash-onto")
+
+/*
+ * The path of the file listing refs that need to be deleted after the rebase
+ * finishes. This is used by the `label` command to record the need for cleanup.
+ */
+static GIT_PATH_FUNC(rebase_path_refs_to_delete, "rebase-merge/refs-to-delete")
+
/*
* The following files are written by git-rebase just after parsing the
* command-line (and are only consumed, not modified, by the sequencer).
int sequencer_remove_state(struct replay_opts *opts)
{
- struct strbuf dir = STRBUF_INIT;
+ struct strbuf buf = STRBUF_INIT;
int i;
+ if (is_rebase_i(opts) &&
+ strbuf_read_file(&buf, rebase_path_refs_to_delete(), 0) > 0) {
+ char *p = buf.buf;
+ while (*p) {
+ char *eol = strchr(p, '\n');
+ if (eol)
+ *eol = '\0';
+ if (delete_ref("(rebase -i) cleanup", p, NULL, 0) < 0)
+ warning(_("could not delete '%s'"), p);
+ if (!eol)
+ break;
+ p = eol + 1;
+ }
+ }
+
free(opts->gpg_sign);
free(opts->strategy);
for (i = 0; i < opts->xopts_nr; i++)
free(opts->xopts[i]);
free(opts->xopts);
+ strbuf_release(&opts->current_fixups);
- strbuf_addstr(&dir, get_dir(opts));
- remove_dir_recursively(&dir, 0);
- strbuf_release(&dir);
+ strbuf_reset(&buf);
+ strbuf_addstr(&buf, get_dir(opts));
+ remove_dir_recursively(&buf, 0);
+ strbuf_release(&buf);
return 0;
}
if (msg_fd < 0)
return error_errno(_("could not lock '%s'"), filename);
if (write_in_full(msg_fd, buf, len) < 0) {
+ error_errno(_("could not write to '%s'"), filename);
rollback_lock_file(&msg_file);
- return error_errno(_("could not write to '%s'"), filename);
+ return -1;
}
if (append_eol && write(msg_fd, "\n", 1) < 0) {
+ error_errno(_("could not write eol to '%s'"), filename);
rollback_lock_file(&msg_file);
- return error_errno(_("could not write eol to '%s'"), filename);
+ return -1;
}
if (commit_lock_file(&msg_file) < 0)
return error(_("failed to finalize '%s'"), filename);
transaction = ref_transaction_begin(&err);
if (!transaction ||
ref_transaction_update(transaction, "HEAD",
- to, unborn ? &null_oid : from,
+ to, unborn && !is_rebase_i(opts) ?
+ &null_oid : from,
0, sb.buf, &err) ||
ref_transaction_commit(transaction, &err)) {
ref_transaction_free(transaction);
o.show_rename_progress = 1;
head_tree = parse_tree_indirect(head);
- next_tree = next ? next->tree : empty_tree();
- base_tree = base ? base->tree : empty_tree();
+ next_tree = next ? get_commit_tree(next) : empty_tree();
+ base_tree = base ? get_commit_tree(base) : empty_tree();
for (xopt = opts->xopts; xopt != opts->xopts + opts->xopts_nr; xopt++)
parse_merge_opt(&o, *xopt);
return !clean;
}
+static struct object_id *get_cache_tree_oid(void)
+{
+ if (!active_cache_tree)
+ active_cache_tree = cache_tree();
+
+ if (!cache_tree_fully_valid(active_cache_tree))
+ if (cache_tree_update(&the_index, 0)) {
+ error(_("unable to update cache tree"));
+ return NULL;
+ }
+
+ return &active_cache_tree->oid;
+}
+
static int is_index_unchanged(void)
{
- struct object_id head_oid;
+ struct object_id head_oid, *cache_tree_oid;
struct commit *head_commit;
if (!resolve_ref_unsafe("HEAD", RESOLVE_REF_READING, &head_oid, NULL))
if (parse_commit(head_commit))
return -1;
- if (!active_cache_tree)
- active_cache_tree = cache_tree();
-
- if (!cache_tree_fully_valid(active_cache_tree))
- if (cache_tree_update(&the_index, 0))
- return error(_("unable to update cache tree"));
+ if (!(cache_tree_oid = get_cache_tree_oid()))
+ return -1;
- return !oidcmp(&active_cache_tree->oid,
- &head_commit->tree->object.oid);
+ return !oidcmp(cache_tree_oid, get_commit_tree_oid(head_commit));
}
static int write_author_script(const char *message)
return NULL;
}
+/* Read author-script and return an ident line (author <email> timestamp) */
+static const char *read_author_ident(struct strbuf *buf)
+{
+ const char *keys[] = {
+ "GIT_AUTHOR_NAME=", "GIT_AUTHOR_EMAIL=", "GIT_AUTHOR_DATE="
+ };
+ char *in, *out, *eol;
+ int i = 0, len;
+
+ if (strbuf_read_file(buf, rebase_path_author_script(), 256) <= 0)
+ return NULL;
+
+ /* dequote values and construct ident line in-place */
+ for (in = out = buf->buf; i < 3 && in - buf->buf < buf->len; i++) {
+ if (!skip_prefix(in, keys[i], (const char **)&in)) {
+ warning("could not parse '%s' (looking for '%s'",
+ rebase_path_author_script(), keys[i]);
+ return NULL;
+ }
+
+ eol = strchrnul(in, '\n');
+ *eol = '\0';
+ sq_dequote(in);
+ len = strlen(in);
+
+ if (i > 0) /* separate values by spaces */
+ *(out++) = ' ';
+ if (i == 1) /* email needs to be surrounded by <...> */
+ *(out++) = '<';
+ memmove(out, in, len);
+ out += len;
+ if (i == 1) /* email needs to be surrounded by <...> */
+ *(out++) = '>';
+ in = eol + 1;
+ }
+
+ if (i < 3) {
+ warning("could not parse '%s' (looking for '%s')",
+ rebase_path_author_script(), keys[i]);
+ return NULL;
+ }
+
+ buf->len = out - buf->buf;
+ return buf->buf;
+}
+
static const char staged_changes_advice[] =
N_("you have staged changes in your working tree\n"
"If these changes are meant to be squashed into the previous commit, run:\n"
#define AMEND_MSG (1<<2)
#define CLEANUP_MSG (1<<3)
#define VERIFY_MSG (1<<4)
+#define CREATE_ROOT_COMMIT (1<<5)
/*
* If we are cherry-pick, and if the merge did not result in
struct child_process cmd = CHILD_PROCESS_INIT;
const char *value;
+ if (flags & CREATE_ROOT_COMMIT) {
+ struct strbuf msg = STRBUF_INIT, script = STRBUF_INIT;
+ const char *author = is_rebase_i(opts) ?
+ read_author_ident(&script) : NULL;
+ struct object_id root_commit, *cache_tree_oid;
+ int res = 0;
+
+ if (!defmsg)
+ BUG("root commit without message");
+
+ if (!(cache_tree_oid = get_cache_tree_oid()))
+ res = -1;
+
+ if (!res)
+ res = strbuf_read_file(&msg, defmsg, 0);
+
+ if (res <= 0)
+ res = error_errno(_("could not read '%s'"), defmsg);
+ else
+ res = commit_tree(msg.buf, msg.len, cache_tree_oid,
+ NULL, &root_commit, author,
+ opts->gpg_sign);
+
+ strbuf_release(&msg);
+ strbuf_release(&script);
+ if (!res) {
+ update_ref(NULL, "CHERRY_PICK_HEAD", &root_commit, NULL,
+ REF_NO_DEREF, UPDATE_REFS_MSG_ON_ERR);
+ res = update_ref(NULL, "HEAD", &root_commit, NULL, 0,
+ UPDATE_REFS_MSG_ON_ERR);
+ }
+ return res < 0 ? error(_("writing root commit")) : 0;
+ }
+
cmd.git_cmd = 1;
if (is_rebase_i(opts)) {
argv_array_pushf(&cmd.args, "-S%s", opts->gpg_sign);
if (defmsg)
argv_array_pushl(&cmd.args, "-F", defmsg, NULL);
+ else if (!(flags & EDIT_MSG))
+ argv_array_pushl(&cmd.args, "-C", "HEAD", NULL);
if ((flags & CLEANUP_MSG))
argv_array_push(&cmd.args, "--cleanup=strip");
if ((flags & EDIT_MSG))
}
if (!(flags & ALLOW_EMPTY) && !oidcmp(current_head ?
- ¤t_head->tree->object.oid :
- &empty_tree_oid, &tree)) {
+ get_commit_tree_oid(current_head) :
+ the_hash_algo->empty_tree, &tree)) {
res = 1; /* run 'git commit' to display error message */
goto out;
}
{
int res = 1;
- if (!(flags & EDIT_MSG) && !(flags & VERIFY_MSG)) {
+ if (!(flags & EDIT_MSG) && !(flags & VERIFY_MSG) &&
+ !(flags & CREATE_ROOT_COMMIT)) {
struct object_id oid;
struct strbuf sb = STRBUF_INIT;
if (parse_commit(parent))
return error(_("could not parse parent commit %s"),
oid_to_hex(&parent->object.oid));
- ptree_oid = &parent->tree->object.oid;
+ ptree_oid = get_commit_tree_oid(parent);
} else {
ptree_oid = the_hash_algo->empty_tree; /* commit is root */
}
- return !oidcmp(ptree_oid, &commit->tree->object.oid);
+ return !oidcmp(ptree_oid, get_commit_tree_oid(commit));
}
/*
TODO_SQUASH,
/* commands that do something else than handling a single commit */
TODO_EXEC,
+ TODO_LABEL,
+ TODO_RESET,
+ TODO_MERGE,
/* commands that do nothing but are counted for reporting progress */
TODO_NOOP,
TODO_DROP,
{ 'f', "fixup" },
{ 's', "squash" },
{ 'x', "exec" },
+ { 'l', "label" },
+ { 't', "reset" },
+ { 'm', "merge" },
{ 0, "noop" },
{ 'd', "drop" },
{ 0, NULL }
return command == TODO_FIXUP || command == TODO_SQUASH;
}
+/* Does this command create a (non-merge) commit? */
+static int is_pick_or_similar(enum todo_command command)
+{
+ switch (command) {
+ case TODO_PICK:
+ case TODO_REVERT:
+ case TODO_EDIT:
+ case TODO_REWORD:
+ case TODO_FIXUP:
+ case TODO_SQUASH:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
static int update_squash_messages(enum todo_command command,
struct commit *commit, struct replay_opts *opts)
{
struct strbuf buf = STRBUF_INIT;
- int count, res;
+ int res;
const char *message, *body;
- if (file_exists(rebase_path_squash_msg())) {
+ if (opts->current_fixup_count > 0) {
struct strbuf header = STRBUF_INIT;
- char *eol, *p;
+ char *eol;
- if (strbuf_read_file(&buf, rebase_path_squash_msg(), 2048) <= 0)
+ if (strbuf_read_file(&buf, rebase_path_squash_msg(), 9) <= 0)
return error(_("could not read '%s'"),
rebase_path_squash_msg());
- p = buf.buf + 1;
- eol = strchrnul(buf.buf, '\n');
- if (buf.buf[0] != comment_line_char ||
- (p += strcspn(p, "0123456789\n")) == eol)
- return error(_("unexpected 1st line of squash message:"
- "\n\n\t%.*s"),
- (int)(eol - buf.buf), buf.buf);
- count = strtol(p, NULL, 10);
-
- if (count < 1)
- return error(_("invalid 1st line of squash message:\n"
- "\n\t%.*s"),
- (int)(eol - buf.buf), buf.buf);
+ eol = buf.buf[0] != comment_line_char ?
+ buf.buf : strchrnul(buf.buf, '\n');
strbuf_addf(&header, "%c ", comment_line_char);
- strbuf_addf(&header,
- _("This is a combination of %d commits."), ++count);
+ strbuf_addf(&header, _("This is a combination of %d commits."),
+ opts->current_fixup_count + 2);
strbuf_splice(&buf, 0, eol - buf.buf, header.buf, header.len);
strbuf_release(&header);
} else {
rebase_path_fixup_msg());
}
- count = 2;
strbuf_addf(&buf, "%c ", comment_line_char);
- strbuf_addf(&buf, _("This is a combination of %d commits."),
- count);
+ strbuf_addf(&buf, _("This is a combination of %d commits."), 2);
strbuf_addf(&buf, "\n%c ", comment_line_char);
strbuf_addstr(&buf, _("This is the 1st commit message:"));
strbuf_addstr(&buf, "\n\n");
if (command == TODO_SQUASH) {
unlink(rebase_path_fixup_msg());
strbuf_addf(&buf, "\n%c ", comment_line_char);
- strbuf_addf(&buf, _("This is the commit message #%d:"), count);
+ strbuf_addf(&buf, _("This is the commit message #%d:"),
+ ++opts->current_fixup_count);
strbuf_addstr(&buf, "\n\n");
strbuf_addstr(&buf, body);
} else if (command == TODO_FIXUP) {
strbuf_addf(&buf, "\n%c ", comment_line_char);
strbuf_addf(&buf, _("The commit message #%d will be skipped:"),
- count);
+ ++opts->current_fixup_count);
strbuf_addstr(&buf, "\n\n");
strbuf_add_commented_lines(&buf, body, strlen(body));
} else
res = write_message(buf.buf, buf.len, rebase_path_squash_msg(), 0);
strbuf_release(&buf);
+
+ if (!res) {
+ strbuf_addf(&opts->current_fixups, "%s%s %s",
+ opts->current_fixups.len ? "\n" : "",
+ command_to_string(command),
+ oid_to_hex(&commit->object.oid));
+ res = write_message(opts->current_fixups.buf,
+ opts->current_fixups.len,
+ rebase_path_current_fixups(), 0);
+ }
+
return res;
}
return error(_("your index file is unmerged."));
} else {
unborn = get_oid("HEAD", &head);
- if (unborn)
+ /* Do we want to generate a root commit? */
+ if (is_pick_or_similar(command) && opts->have_squash_onto &&
+ !oidcmp(&head, &opts->squash_onto)) {
+ if (is_fixup(command))
+ return error(_("cannot fixup root commit"));
+ flags |= CREATE_ROOT_COMMIT;
+ unborn = 1;
+ } else if (unborn)
oidcpy(&head, the_hash_algo->empty_tree);
- if (index_differs_from(unborn ? EMPTY_TREE_SHA1_HEX : "HEAD",
+ if (index_differs_from(unborn ? empty_tree_oid_hex() : "HEAD",
NULL, 0))
return error_dirty_index(opts);
}
if (!res && final_fixup) {
unlink(rebase_path_fixup_msg());
unlink(rebase_path_squash_msg());
+ unlink(rebase_path_current_fixups());
+ strbuf_reset(&opts->current_fixups);
+ opts->current_fixup_count = 0;
}
leave:
return 0;
}
+enum todo_item_flags {
+ TODO_EDIT_MERGE_MSG = 1
+};
+
struct todo_item {
enum todo_command command;
struct commit *commit;
+ unsigned int flags;
const char *arg;
int arg_len;
size_t offset_in_buf;
char *end_of_object_name;
int i, saved, status, padding;
+ item->flags = 0;
+
/* left-trim */
bol += strspn(bol, " \t");
return error(_("missing arguments for %s"),
command_to_string(item->command));
- if (item->command == TODO_EXEC) {
+ if (item->command == TODO_EXEC || item->command == TODO_LABEL ||
+ item->command == TODO_RESET) {
item->commit = NULL;
item->arg = bol;
item->arg_len = (int)(eol - bol);
return 0;
}
+ if (item->command == TODO_MERGE) {
+ if (skip_prefix(bol, "-C", &bol))
+ bol += strspn(bol, " \t");
+ else if (skip_prefix(bol, "-c", &bol)) {
+ bol += strspn(bol, " \t");
+ item->flags |= TODO_EDIT_MERGE_MSG;
+ } else {
+ item->flags |= TODO_EDIT_MERGE_MSG;
+ item->commit = NULL;
+ item->arg = bol;
+ item->arg_len = (int)(eol - bol);
+ return 0;
+ }
+ }
+
end_of_object_name = (char *) bol + strcspn(bol, " \t\n");
saved = *end_of_object_name;
*end_of_object_name = '\0';
return count;
}
+static int get_item_line_offset(struct todo_list *todo_list, int index)
+{
+ return index < todo_list->nr ?
+ todo_list->items[index].offset_in_buf : todo_list->buf.len;
+}
+
+static const char *get_item_line(struct todo_list *todo_list, int index)
+{
+ return todo_list->buf.buf + get_item_line_offset(todo_list, index);
+}
+
+static int get_item_line_length(struct todo_list *todo_list, int index)
+{
+ return get_item_line_offset(todo_list, index + 1)
+ - get_item_line_offset(todo_list, index);
+}
+
static ssize_t strbuf_read_file_or_whine(struct strbuf *sb, const char *path)
{
int fd;
read_strategy_opts(opts, &buf);
strbuf_release(&buf);
+ if (read_oneliner(&opts->current_fixups,
+ rebase_path_current_fixups(), 1)) {
+ const char *p = opts->current_fixups.buf;
+ opts->current_fixup_count = 1;
+ while ((p = strchr(p, '\n'))) {
+ opts->current_fixup_count++;
+ p++;
+ }
+ }
+
+ if (read_oneliner(&buf, rebase_path_squash_onto(), 0)) {
+ if (get_oid_hex(buf.buf, &opts->squash_onto) < 0)
+ return error(_("unusable squash-onto"));
+ opts->have_squash_onto = 1;
+ }
+
return 0;
}
written = write_in_full(fd, buf.buf, buf.len);
strbuf_release(&buf);
if (written < 0) {
+ error_errno(_("could not write to '%s'"), git_path_head_file());
rollback_lock_file(&head_lock);
- return error_errno(_("could not write to '%s'"),
- git_path_head_file());
+ return -1;
}
if (commit_lock_file(&head_lock) < 0)
return error(_("failed to finalize '%s'"), git_path_head_file());
fd = hold_lock_file_for_update(&todo_lock, todo_path, 0);
if (fd < 0)
return error_errno(_("could not lock '%s'"), todo_path);
- offset = next < todo_list->nr ?
- todo_list->items[next].offset_in_buf : todo_list->buf.len;
+ offset = get_item_line_offset(todo_list, next);
if (write_in_full(fd, todo_list->buf.buf + offset,
todo_list->buf.len - offset) < 0)
return error_errno(_("could not write to '%s'"), todo_path);
if (commit_lock_file(&todo_lock) < 0)
return error(_("failed to finalize '%s'"), todo_path);
- if (is_rebase_i(opts)) {
- const char *done_path = rebase_path_done();
- int fd = open(done_path, O_CREAT | O_WRONLY | O_APPEND, 0666);
- int prev_offset = !next ? 0 :
- todo_list->items[next - 1].offset_in_buf;
+ if (is_rebase_i(opts) && next > 0) {
+ const char *done = rebase_path_done();
+ int fd = open(done, O_CREAT | O_WRONLY | O_APPEND, 0666);
+ int ret = 0;
- if (fd >= 0 && offset > prev_offset &&
- write_in_full(fd, todo_list->buf.buf + prev_offset,
- offset - prev_offset) < 0) {
- close(fd);
- return error_errno(_("could not write to '%s'"),
- done_path);
- }
- if (fd >= 0)
- close(fd);
+ if (fd < 0)
+ return 0;
+ if (write_in_full(fd, get_item_line(todo_list, next - 1),
+ get_item_line_length(todo_list, next - 1))
+ < 0)
+ ret = error_errno(_("could not write to '%s'"), done);
+ if (close(fd) < 0)
+ ret = error_errno(_("failed to finalize '%s'"), done);
+ return ret;
}
return 0;
}
static int error_failed_squash(struct commit *commit,
struct replay_opts *opts, int subject_len, const char *subject)
{
- if (rename(rebase_path_squash_msg(), rebase_path_message()))
- return error(_("could not rename '%s' to '%s'"),
+ if (copy_file(rebase_path_message(), rebase_path_squash_msg(), 0666))
+ return error(_("could not copy '%s' to '%s'"),
rebase_path_squash_msg(), rebase_path_message());
- unlink(rebase_path_fixup_msg());
unlink(git_path_merge_msg());
if (copy_file(git_path_merge_msg(), rebase_path_message(), 0666))
return error(_("could not copy '%s' to '%s'"),
return status;
}
+static int safe_append(const char *filename, const char *fmt, ...)
+{
+ va_list ap;
+ struct lock_file lock = LOCK_INIT;
+ int fd = hold_lock_file_for_update(&lock, filename,
+ LOCK_REPORT_ON_ERROR);
+ struct strbuf buf = STRBUF_INIT;
+
+ if (fd < 0)
+ return -1;
+
+ if (strbuf_read_file(&buf, filename, 0) < 0 && errno != ENOENT) {
+ error_errno(_("could not read '%s'"), filename);
+ rollback_lock_file(&lock);
+ return -1;
+ }
+ strbuf_complete(&buf, '\n');
+ va_start(ap, fmt);
+ strbuf_vaddf(&buf, fmt, ap);
+ va_end(ap);
+
+ if (write_in_full(fd, buf.buf, buf.len) < 0) {
+ error_errno(_("could not write to '%s'"), filename);
+ strbuf_release(&buf);
+ rollback_lock_file(&lock);
+ return -1;
+ }
+ if (commit_lock_file(&lock) < 0) {
+ strbuf_release(&buf);
+ rollback_lock_file(&lock);
+ return error(_("failed to finalize '%s'"), filename);
+ }
+
+ strbuf_release(&buf);
+ return 0;
+}
+
+static int do_label(const char *name, int len)
+{
+ struct ref_store *refs = get_main_ref_store(the_repository);
+ struct ref_transaction *transaction;
+ struct strbuf ref_name = STRBUF_INIT, err = STRBUF_INIT;
+ struct strbuf msg = STRBUF_INIT;
+ int ret = 0;
+ struct object_id head_oid;
+
+ if (len == 1 && *name == '#')
+ return error("Illegal label name: '%.*s'", len, name);
+
+ strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
+ strbuf_addf(&msg, "rebase -i (label) '%.*s'", len, name);
+
+ transaction = ref_store_transaction_begin(refs, &err);
+ if (!transaction) {
+ error("%s", err.buf);
+ ret = -1;
+ } else if (get_oid("HEAD", &head_oid)) {
+ error(_("could not read HEAD"));
+ ret = -1;
+ } else if (ref_transaction_update(transaction, ref_name.buf, &head_oid,
+ NULL, 0, msg.buf, &err) < 0 ||
+ ref_transaction_commit(transaction, &err)) {
+ error("%s", err.buf);
+ ret = -1;
+ }
+ ref_transaction_free(transaction);
+ strbuf_release(&err);
+ strbuf_release(&msg);
+
+ if (!ret)
+ ret = safe_append(rebase_path_refs_to_delete(),
+ "%s\n", ref_name.buf);
+ strbuf_release(&ref_name);
+
+ return ret;
+}
+
+static const char *reflog_message(struct replay_opts *opts,
+ const char *sub_action, const char *fmt, ...);
+
+static int do_reset(const char *name, int len, struct replay_opts *opts)
+{
+ struct strbuf ref_name = STRBUF_INIT;
+ struct object_id oid;
+ struct lock_file lock = LOCK_INIT;
+ struct tree_desc desc;
+ struct tree *tree;
+ struct unpack_trees_options unpack_tree_opts;
+ int ret = 0, i;
+
+ if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0)
+ return -1;
+
+ if (len == 10 && !strncmp("[new root]", name, len)) {
+ if (!opts->have_squash_onto) {
+ const char *hex;
+ if (commit_tree("", 0, the_hash_algo->empty_tree,
+ NULL, &opts->squash_onto,
+ NULL, NULL))
+ return error(_("writing fake root commit"));
+ opts->have_squash_onto = 1;
+ hex = oid_to_hex(&opts->squash_onto);
+ if (write_message(hex, strlen(hex),
+ rebase_path_squash_onto(), 0))
+ return error(_("writing squash-onto"));
+ }
+ oidcpy(&oid, &opts->squash_onto);
+ } else {
+ /* Determine the length of the label */
+ for (i = 0; i < len; i++)
+ if (isspace(name[i]))
+ len = i;
+
+ strbuf_addf(&ref_name, "refs/rewritten/%.*s", len, name);
+ if (get_oid(ref_name.buf, &oid) &&
+ get_oid(ref_name.buf + strlen("refs/rewritten/"), &oid)) {
+ error(_("could not read '%s'"), ref_name.buf);
+ rollback_lock_file(&lock);
+ strbuf_release(&ref_name);
+ return -1;
+ }
+ }
+
+ memset(&unpack_tree_opts, 0, sizeof(unpack_tree_opts));
+ setup_unpack_trees_porcelain(&unpack_tree_opts, "reset");
+ unpack_tree_opts.head_idx = 1;
+ unpack_tree_opts.src_index = &the_index;
+ unpack_tree_opts.dst_index = &the_index;
+ unpack_tree_opts.fn = oneway_merge;
+ unpack_tree_opts.merge = 1;
+ unpack_tree_opts.update = 1;
+
+ if (read_cache_unmerged()) {
+ rollback_lock_file(&lock);
+ strbuf_release(&ref_name);
+ return error_resolve_conflict(_(action_name(opts)));
+ }
+
+ if (!fill_tree_descriptor(&desc, &oid)) {
+ error(_("failed to find tree of %s"), oid_to_hex(&oid));
+ rollback_lock_file(&lock);
+ free((void *)desc.buffer);
+ strbuf_release(&ref_name);
+ return -1;
+ }
+
+ if (unpack_trees(1, &desc, &unpack_tree_opts)) {
+ rollback_lock_file(&lock);
+ free((void *)desc.buffer);
+ strbuf_release(&ref_name);
+ return -1;
+ }
+
+ tree = parse_tree_indirect(&oid);
+ prime_cache_tree(&the_index, tree);
+
+ if (write_locked_index(&the_index, &lock, COMMIT_LOCK) < 0)
+ ret = error(_("could not write index"));
+ free((void *)desc.buffer);
+
+ if (!ret)
+ ret = update_ref(reflog_message(opts, "reset", "'%.*s'",
+ len, name), "HEAD", &oid,
+ NULL, 0, UPDATE_REFS_MSG_ON_ERR);
+
+ strbuf_release(&ref_name);
+ return ret;
+}
+
+static int do_merge(struct commit *commit, const char *arg, int arg_len,
+ int flags, struct replay_opts *opts)
+{
+ int run_commit_flags = (flags & TODO_EDIT_MERGE_MSG) ?
+ EDIT_MSG | VERIFY_MSG : 0;
+ struct strbuf ref_name = STRBUF_INIT;
+ struct commit *head_commit, *merge_commit, *i;
+ struct commit_list *bases, *j, *reversed = NULL;
+ struct merge_options o;
+ int merge_arg_len, oneline_offset, can_fast_forward, ret;
+ static struct lock_file lock;
+ const char *p;
+
+ if (hold_locked_index(&lock, LOCK_REPORT_ON_ERROR) < 0) {
+ ret = -1;
+ goto leave_merge;
+ }
+
+ head_commit = lookup_commit_reference_by_name("HEAD");
+ if (!head_commit) {
+ ret = error(_("cannot merge without a current revision"));
+ goto leave_merge;
+ }
+
+ oneline_offset = arg_len;
+ merge_arg_len = strcspn(arg, " \t\n");
+ p = arg + merge_arg_len;
+ p += strspn(p, " \t\n");
+ if (*p == '#' && (!p[1] || isspace(p[1]))) {
+ p += 1 + strspn(p + 1, " \t\n");
+ oneline_offset = p - arg;
+ } else if (p - arg < arg_len)
+ BUG("octopus merges are not supported yet: '%s'", p);
+
+ strbuf_addf(&ref_name, "refs/rewritten/%.*s", merge_arg_len, arg);
+ merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+ if (!merge_commit) {
+ /* fall back to non-rewritten ref or commit */
+ strbuf_splice(&ref_name, 0, strlen("refs/rewritten/"), "", 0);
+ merge_commit = lookup_commit_reference_by_name(ref_name.buf);
+ }
+
+ if (!merge_commit) {
+ ret = error(_("could not resolve '%s'"), ref_name.buf);
+ goto leave_merge;
+ }
+
+ if (opts->have_squash_onto &&
+ !oidcmp(&head_commit->object.oid, &opts->squash_onto)) {
+ /*
+ * When the user tells us to "merge" something into a
+ * "[new root]", let's simply fast-forward to the merge head.
+ */
+ rollback_lock_file(&lock);
+ ret = fast_forward_to(&merge_commit->object.oid,
+ &head_commit->object.oid, 0, opts);
+ goto leave_merge;
+ }
+
+ if (commit) {
+ const char *message = get_commit_buffer(commit, NULL);
+ const char *body;
+ int len;
+
+ if (!message) {
+ ret = error(_("could not get commit message of '%s'"),
+ oid_to_hex(&commit->object.oid));
+ goto leave_merge;
+ }
+ write_author_script(message);
+ find_commit_subject(message, &body);
+ len = strlen(body);
+ ret = write_message(body, len, git_path_merge_msg(), 0);
+ unuse_commit_buffer(commit, message);
+ if (ret) {
+ error_errno(_("could not write '%s'"),
+ git_path_merge_msg());
+ goto leave_merge;
+ }
+ } else {
+ struct strbuf buf = STRBUF_INIT;
+ int len;
+
+ strbuf_addf(&buf, "author %s", git_author_info(0));
+ write_author_script(buf.buf);
+ strbuf_reset(&buf);
+
+ if (oneline_offset < arg_len) {
+ p = arg + oneline_offset;
+ len = arg_len - oneline_offset;
+ } else {
+ strbuf_addf(&buf, "Merge branch '%.*s'",
+ merge_arg_len, arg);
+ p = buf.buf;
+ len = buf.len;
+ }
+
+ ret = write_message(p, len, git_path_merge_msg(), 0);
+ strbuf_release(&buf);
+ if (ret) {
+ error_errno(_("could not write '%s'"),
+ git_path_merge_msg());
+ goto leave_merge;
+ }
+ }
+
+ /*
+ * If HEAD is not identical to the first parent of the original merge
+ * commit, we cannot fast-forward.
+ */
+ can_fast_forward = opts->allow_ff && commit && commit->parents &&
+ !oidcmp(&commit->parents->item->object.oid,
+ &head_commit->object.oid);
+
+ /*
+ * If the merge head is different from the original one, we cannot
+ * fast-forward.
+ */
+ if (can_fast_forward) {
+ struct commit_list *second_parent = commit->parents->next;
+
+ if (second_parent && !second_parent->next &&
+ oidcmp(&merge_commit->object.oid,
+ &second_parent->item->object.oid))
+ can_fast_forward = 0;
+ }
+
+ if (can_fast_forward && commit->parents->next &&
+ !commit->parents->next->next &&
+ !oidcmp(&commit->parents->next->item->object.oid,
+ &merge_commit->object.oid)) {
+ rollback_lock_file(&lock);
+ ret = fast_forward_to(&commit->object.oid,
+ &head_commit->object.oid, 0, opts);
+ goto leave_merge;
+ }
+
+ write_message(oid_to_hex(&merge_commit->object.oid), GIT_SHA1_HEXSZ,
+ git_path_merge_head(), 0);
+ write_message("no-ff", 5, git_path_merge_mode(), 0);
+
+ bases = get_merge_bases(head_commit, merge_commit);
+ if (bases && !oidcmp(&merge_commit->object.oid,
+ &bases->item->object.oid)) {
+ ret = 0;
+ /* skip merging an ancestor of HEAD */
+ goto leave_merge;
+ }
+
+ for (j = bases; j; j = j->next)
+ commit_list_insert(j->item, &reversed);
+ free_commit_list(bases);
+
+ read_cache();
+ init_merge_options(&o);
+ o.branch1 = "HEAD";
+ o.branch2 = ref_name.buf;
+ o.buffer_output = 2;
+
+ ret = merge_recursive(&o, head_commit, merge_commit, reversed, &i);
+ if (ret <= 0)
+ fputs(o.obuf.buf, stdout);
+ strbuf_release(&o.obuf);
+ if (ret < 0) {
+ error(_("could not even attempt to merge '%.*s'"),
+ merge_arg_len, arg);
+ goto leave_merge;
+ }
+ /*
+ * The return value of merge_recursive() is 1 on clean, and 0 on
+ * unclean merge.
+ *
+ * Let's reverse that, so that do_merge() returns 0 upon success and
+ * 1 upon failed merge (keeping the return value -1 for the cases where
+ * we will want to reschedule the `merge` command).
+ */
+ ret = !ret;
+
+ if (active_cache_changed &&
+ write_locked_index(&the_index, &lock, COMMIT_LOCK)) {
+ ret = error(_("merge: Unable to write new index file"));
+ goto leave_merge;
+ }
+
+ rollback_lock_file(&lock);
+ if (ret)
+ rerere(opts->allow_rerere_auto);
+ else
+ /*
+ * In case of problems, we now want to return a positive
+ * value (a negative one would indicate that the `merge`
+ * command needs to be rescheduled).
+ */
+ ret = !!run_git_commit(git_path_merge_msg(), opts,
+ run_commit_flags);
+
+leave_merge:
+ strbuf_release(&ref_name);
+ rollback_lock_file(&lock);
+ return ret;
+}
+
static int is_final_fixup(struct todo_list *todo_list)
{
int i = todo_list->current;
return buf.buf;
}
+static const char rescheduled_advice[] =
+N_("Could not execute the todo command\n"
+"\n"
+" %.*s"
+"\n"
+"It has been rescheduled; To edit the command before continuing, please\n"
+"edit the todo list first:\n"
+"\n"
+" git rebase --edit-todo\n"
+" git rebase --continue\n");
+
static int pick_commits(struct todo_list *todo_list, struct replay_opts *opts)
{
- int res = 0;
+ int res = 0, reschedule = 0;
setenv(GIT_REFLOG_ACTION, action_name(opts), 0);
if (opts->allow_ff)
opts, is_final_fixup(todo_list));
if (is_rebase_i(opts) && res < 0) {
/* Reschedule */
+ advise(_(rescheduled_advice),
+ get_item_line_length(todo_list,
+ todo_list->current),
+ get_item_line(todo_list,
+ todo_list->current));
todo_list->current--;
if (save_todo(todo_list, opts))
return -1;
intend_to_amend();
return error_failed_squash(item->commit, opts,
item->arg_len, item->arg);
- } else if (res && is_rebase_i(opts))
+ } else if (res && is_rebase_i(opts) && item->commit)
return res | error_with_patch(item->commit,
item->arg, item->arg_len, opts, res,
item->command == TODO_REWORD);
/* `current` will be incremented below */
todo_list->current = -1;
}
+ } else if (item->command == TODO_LABEL) {
+ if ((res = do_label(item->arg, item->arg_len)))
+ reschedule = 1;
+ } else if (item->command == TODO_RESET) {
+ if ((res = do_reset(item->arg, item->arg_len, opts)))
+ reschedule = 1;
+ } else if (item->command == TODO_MERGE) {
+ if ((res = do_merge(item->commit,
+ item->arg, item->arg_len,
+ item->flags, opts)) < 0)
+ reschedule = 1;
+ else if (item->commit)
+ record_in_rewritten(&item->commit->object.oid,
+ peek_command(todo_list, 1));
+ if (res > 0)
+ /* failed with merge conflicts */
+ return error_with_patch(item->commit,
+ item->arg,
+ item->arg_len, opts,
+ res, 0);
} else if (!is_noop(item->command))
return error(_("unknown command %d"), item->command);
+ if (reschedule) {
+ advise(_(rescheduled_advice),
+ get_item_line_length(todo_list,
+ todo_list->current),
+ get_item_line(todo_list, todo_list->current));
+ todo_list->current--;
+ if (save_todo(todo_list, opts))
+ return -1;
+ if (item->commit)
+ return error_with_patch(item->commit,
+ item->arg,
+ item->arg_len, opts,
+ res, 0);
+ }
+
todo_list->current++;
if (res)
return res;
return run_command_v_opt(argv, RUN_GIT_CMD);
}
-static int commit_staged_changes(struct replay_opts *opts)
+static int commit_staged_changes(struct replay_opts *opts,
+ struct todo_list *todo_list)
{
unsigned int flags = ALLOW_EMPTY | EDIT_MSG;
+ unsigned int final_fixup = 0, is_clean;
if (has_unstaged_changes(1))
return error(_("cannot rebase: You have unstaged changes."));
- if (!has_uncommitted_changes(0)) {
- const char *cherry_pick_head = git_path_cherry_pick_head();
- if (file_exists(cherry_pick_head) && unlink(cherry_pick_head))
- return error(_("could not remove CHERRY_PICK_HEAD"));
- return 0;
- }
+ is_clean = !has_uncommitted_changes(0);
if (file_exists(rebase_path_amend())) {
struct strbuf rev = STRBUF_INIT;
if (get_oid_hex(rev.buf, &to_amend))
return error(_("invalid contents: '%s'"),
rebase_path_amend());
- if (oidcmp(&head, &to_amend))
+ if (!is_clean && oidcmp(&head, &to_amend))
return error(_("\nYou have uncommitted changes in your "
"working tree. Please, commit them\n"
"first and then run 'git rebase "
"--continue' again."));
+ /*
+ * When skipping a failed fixup/squash, we need to edit the
+ * commit message, the current fixup list and count, and if it
+ * was the last fixup/squash in the chain, we need to clean up
+ * the commit message and if there was a squash, let the user
+ * edit it.
+ */
+ if (is_clean && !oidcmp(&head, &to_amend) &&
+ opts->current_fixup_count > 0 &&
+ file_exists(rebase_path_stopped_sha())) {
+ const char *p = opts->current_fixups.buf;
+ int len = opts->current_fixups.len;
+
+ opts->current_fixup_count--;
+ if (!len)
+ BUG("Incorrect current_fixups:\n%s", p);
+ while (len && p[len - 1] != '\n')
+ len--;
+ strbuf_setlen(&opts->current_fixups, len);
+ if (write_message(p, len, rebase_path_current_fixups(),
+ 0) < 0)
+ return error(_("could not write file: '%s'"),
+ rebase_path_current_fixups());
+
+ /*
+ * If a fixup/squash in a fixup/squash chain failed, the
+ * commit message is already correct, no need to commit
+ * it again.
+ *
+ * Only if it is the final command in the fixup/squash
+ * chain, and only if the chain is longer than a single
+ * fixup/squash command (which was just skipped), do we
+ * actually need to re-commit with a cleaned up commit
+ * message.
+ */
+ if (opts->current_fixup_count > 0 &&
+ !is_fixup(peek_command(todo_list, 0))) {
+ final_fixup = 1;
+ /*
+ * If there was not a single "squash" in the
+ * chain, we only need to clean up the commit
+ * message, no need to bother the user with
+ * opening the commit message in the editor.
+ */
+ if (!starts_with(p, "squash ") &&
+ !strstr(p, "\nsquash "))
+ flags = (flags & ~EDIT_MSG) | CLEANUP_MSG;
+ } else if (is_fixup(peek_command(todo_list, 0))) {
+ /*
+ * We need to update the squash message to skip
+ * the latest commit message.
+ */
+ struct commit *commit;
+ const char *path = rebase_path_squash_msg();
+
+ if (parse_head(&commit) ||
+ !(p = get_commit_buffer(commit, NULL)) ||
+ write_message(p, strlen(p), path, 0)) {
+ unuse_commit_buffer(commit, p);
+ return error(_("could not write file: "
+ "'%s'"), path);
+ }
+ unuse_commit_buffer(commit, p);
+ }
+ }
strbuf_release(&rev);
flags |= AMEND_MSG;
}
- if (run_git_commit(rebase_path_message(), opts, flags))
+ if (is_clean) {
+ const char *cherry_pick_head = git_path_cherry_pick_head();
+
+ if (file_exists(cherry_pick_head) && unlink(cherry_pick_head))
+ return error(_("could not remove CHERRY_PICK_HEAD"));
+ if (!final_fixup)
+ return 0;
+ }
+
+ if (run_git_commit(final_fixup ? NULL : rebase_path_message(),
+ opts, flags))
return error(_("could not commit staged changes."));
unlink(rebase_path_amend());
+ if (final_fixup) {
+ unlink(rebase_path_fixup_msg());
+ unlink(rebase_path_squash_msg());
+ }
+ if (opts->current_fixup_count > 0) {
+ /*
+ * Whether final fixup or not, we just cleaned up the commit
+ * message...
+ */
+ unlink(rebase_path_current_fixups());
+ strbuf_reset(&opts->current_fixups);
+ opts->current_fixup_count = 0;
+ }
return 0;
}
if (read_and_refresh_cache(opts))
return -1;
+ if (read_populate_opts(opts))
+ return -1;
if (is_rebase_i(opts)) {
- if (commit_staged_changes(opts))
+ if ((res = read_populate_todo(&todo_list, opts)))
+ goto release_todo_list;
+ if (commit_staged_changes(opts, &todo_list))
return -1;
} else if (!file_exists(get_todo_path(opts)))
return continue_single_pick();
- if (read_populate_opts(opts))
- return -1;
- if ((res = read_populate_todo(&todo_list, opts)))
+ else if ((res = read_populate_todo(&todo_list, opts)))
goto release_todo_list;
if (!is_rebase_i(opts)) {
if (!get_oid(name, &oid)) {
if (!lookup_commit_reference_gently(&oid, 1)) {
- enum object_type type = oid_object_info(&oid,
+ enum object_type type = oid_object_info(the_repository,
+ &oid,
NULL);
return error(_("%s: can't cherry-pick a %s"),
name, type_name(type));
strbuf_release(&sob);
}
+struct labels_entry {
+ struct hashmap_entry entry;
+ char label[FLEX_ARRAY];
+};
+
+static int labels_cmp(const void *fndata, const struct labels_entry *a,
+ const struct labels_entry *b, const void *key)
+{
+ return key ? strcmp(a->label, key) : strcmp(a->label, b->label);
+}
+
+struct string_entry {
+ struct oidmap_entry entry;
+ char string[FLEX_ARRAY];
+};
+
+struct label_state {
+ struct oidmap commit2label;
+ struct hashmap labels;
+ struct strbuf buf;
+};
+
+static const char *label_oid(struct object_id *oid, const char *label,
+ struct label_state *state)
+{
+ struct labels_entry *labels_entry;
+ struct string_entry *string_entry;
+ struct object_id dummy;
+ size_t len;
+ int i;
+
+ string_entry = oidmap_get(&state->commit2label, oid);
+ if (string_entry)
+ return string_entry->string;
+
+ /*
+ * For "uninteresting" commits, i.e. commits that are not to be
+ * rebased, and which can therefore not be labeled, we use a unique
+ * abbreviation of the commit name. This is slightly more complicated
+ * than calling find_unique_abbrev() because we also need to make
+ * sure that the abbreviation does not conflict with any other
+ * label.
+ *
+ * We disallow "interesting" commits to be labeled by a string that
+ * is a valid full-length hash, to ensure that we always can find an
+ * abbreviation for any uninteresting commit's names that does not
+ * clash with any other label.
+ */
+ if (!label) {
+ char *p;
+
+ strbuf_reset(&state->buf);
+ strbuf_grow(&state->buf, GIT_SHA1_HEXSZ);
+ label = p = state->buf.buf;
+
+ find_unique_abbrev_r(p, oid, default_abbrev);
+
+ /*
+ * We may need to extend the abbreviated hash so that there is
+ * no conflicting label.
+ */
+ if (hashmap_get_from_hash(&state->labels, strihash(p), p)) {
+ size_t i = strlen(p) + 1;
+
+ oid_to_hex_r(p, oid);
+ for (; i < GIT_SHA1_HEXSZ; i++) {
+ char save = p[i];
+ p[i] = '\0';
+ if (!hashmap_get_from_hash(&state->labels,
+ strihash(p), p))
+ break;
+ p[i] = save;
+ }
+ }
+ } else if (((len = strlen(label)) == GIT_SHA1_RAWSZ &&
+ !get_oid_hex(label, &dummy)) ||
+ (len == 1 && *label == '#') ||
+ hashmap_get_from_hash(&state->labels,
+ strihash(label), label)) {
+ /*
+ * If the label already exists, or if the label is a valid full
+ * OID, or the label is a '#' (which we use as a separator
+ * between merge heads and oneline), we append a dash and a
+ * number to make it unique.
+ */
+ struct strbuf *buf = &state->buf;
+
+ strbuf_reset(buf);
+ strbuf_add(buf, label, len);
+
+ for (i = 2; ; i++) {
+ strbuf_setlen(buf, len);
+ strbuf_addf(buf, "-%d", i);
+ if (!hashmap_get_from_hash(&state->labels,
+ strihash(buf->buf),
+ buf->buf))
+ break;
+ }
+
+ label = buf->buf;
+ }
+
+ FLEX_ALLOC_STR(labels_entry, label, label);
+ hashmap_entry_init(labels_entry, strihash(label));
+ hashmap_add(&state->labels, labels_entry);
+
+ FLEX_ALLOC_STR(string_entry, string, label);
+ oidcpy(&string_entry->entry.oid, oid);
+ oidmap_put(&state->commit2label, string_entry);
+
+ return string_entry->string;
+}
+
+static int make_script_with_merges(struct pretty_print_context *pp,
+ struct rev_info *revs, FILE *out,
+ unsigned flags)
+{
+ int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
+ int rebase_cousins = flags & TODO_LIST_REBASE_COUSINS;
+ struct strbuf buf = STRBUF_INIT, oneline = STRBUF_INIT;
+ struct strbuf label = STRBUF_INIT;
+ struct commit_list *commits = NULL, **tail = &commits, *iter;
+ struct commit_list *tips = NULL, **tips_tail = &tips;
+ struct commit *commit;
+ struct oidmap commit2todo = OIDMAP_INIT;
+ struct string_entry *entry;
+ struct oidset interesting = OIDSET_INIT, child_seen = OIDSET_INIT,
+ shown = OIDSET_INIT;
+ struct label_state state = { OIDMAP_INIT, { NULL }, STRBUF_INIT };
+
+ int abbr = flags & TODO_LIST_ABBREVIATE_CMDS;
+ const char *cmd_pick = abbr ? "p" : "pick",
+ *cmd_label = abbr ? "l" : "label",
+ *cmd_reset = abbr ? "t" : "reset",
+ *cmd_merge = abbr ? "m" : "merge";
+
+ oidmap_init(&commit2todo, 0);
+ oidmap_init(&state.commit2label, 0);
+ hashmap_init(&state.labels, (hashmap_cmp_fn) labels_cmp, NULL, 0);
+ strbuf_init(&state.buf, 32);
+
+ if (revs->cmdline.nr && (revs->cmdline.rev[0].flags & BOTTOM)) {
+ struct object_id *oid = &revs->cmdline.rev[0].item->oid;
+ FLEX_ALLOC_STR(entry, string, "onto");
+ oidcpy(&entry->entry.oid, oid);
+ oidmap_put(&state.commit2label, entry);
+ }
+
+ /*
+ * First phase:
+ * - get onelines for all commits
+ * - gather all branch tips (i.e. 2nd or later parents of merges)
+ * - label all branch tips
+ */
+ while ((commit = get_revision(revs))) {
+ struct commit_list *to_merge;
+ int is_octopus;
+ const char *p1, *p2;
+ struct object_id *oid;
+ int is_empty;
+
+ tail = &commit_list_insert(commit, tail)->next;
+ oidset_insert(&interesting, &commit->object.oid);
+
+ is_empty = is_original_commit_empty(commit);
+ if (!is_empty && (commit->object.flags & PATCHSAME))
+ continue;
+
+ strbuf_reset(&oneline);
+ pretty_print_commit(pp, commit, &oneline);
+
+ to_merge = commit->parents ? commit->parents->next : NULL;
+ if (!to_merge) {
+ /* non-merge commit: easy case */
+ strbuf_reset(&buf);
+ if (!keep_empty && is_empty)
+ strbuf_addf(&buf, "%c ", comment_line_char);
+ strbuf_addf(&buf, "%s %s %s", cmd_pick,
+ oid_to_hex(&commit->object.oid),
+ oneline.buf);
+
+ FLEX_ALLOC_STR(entry, string, buf.buf);
+ oidcpy(&entry->entry.oid, &commit->object.oid);
+ oidmap_put(&commit2todo, entry);
+
+ continue;
+ }
+
+ is_octopus = to_merge && to_merge->next;
+
+ if (is_octopus)
+ BUG("Octopus merges not yet supported");
+
+ /* Create a label */
+ strbuf_reset(&label);
+ if (skip_prefix(oneline.buf, "Merge ", &p1) &&
+ (p1 = strchr(p1, '\'')) &&
+ (p2 = strchr(++p1, '\'')))
+ strbuf_add(&label, p1, p2 - p1);
+ else if (skip_prefix(oneline.buf, "Merge pull request ",
+ &p1) &&
+ (p1 = strstr(p1, " from ")))
+ strbuf_addstr(&label, p1 + strlen(" from "));
+ else
+ strbuf_addbuf(&label, &oneline);
+
+ for (p1 = label.buf; *p1; p1++)
+ if (isspace(*p1))
+ *(char *)p1 = '-';
+
+ strbuf_reset(&buf);
+ strbuf_addf(&buf, "%s -C %s",
+ cmd_merge, oid_to_hex(&commit->object.oid));
+
+ /* label the tip of merged branch */
+ oid = &to_merge->item->object.oid;
+ strbuf_addch(&buf, ' ');
+
+ if (!oidset_contains(&interesting, oid))
+ strbuf_addstr(&buf, label_oid(oid, NULL, &state));
+ else {
+ tips_tail = &commit_list_insert(to_merge->item,
+ tips_tail)->next;
+
+ strbuf_addstr(&buf, label_oid(oid, label.buf, &state));
+ }
+ strbuf_addf(&buf, " # %s", oneline.buf);
+
+ FLEX_ALLOC_STR(entry, string, buf.buf);
+ oidcpy(&entry->entry.oid, &commit->object.oid);
+ oidmap_put(&commit2todo, entry);
+ }
+
+ /*
+ * Second phase:
+ * - label branch points
+ * - add HEAD to the branch tips
+ */
+ for (iter = commits; iter; iter = iter->next) {
+ struct commit_list *parent = iter->item->parents;
+ for (; parent; parent = parent->next) {
+ struct object_id *oid = &parent->item->object.oid;
+ if (!oidset_contains(&interesting, oid))
+ continue;
+ if (!oidset_contains(&child_seen, oid))
+ oidset_insert(&child_seen, oid);
+ else
+ label_oid(oid, "branch-point", &state);
+ }
+
+ /* Add HEAD as implict "tip of branch" */
+ if (!iter->next)
+ tips_tail = &commit_list_insert(iter->item,
+ tips_tail)->next;
+ }
+
+ /*
+ * Third phase: output the todo list. This is a bit tricky, as we
+ * want to avoid jumping back and forth between revisions. To
+ * accomplish that goal, we walk backwards from the branch tips,
+ * gathering commits not yet shown, reversing the list on the fly,
+ * then outputting that list (labeling revisions as needed).
+ */
+ fprintf(out, "%s onto\n", cmd_label);
+ for (iter = tips; iter; iter = iter->next) {
+ struct commit_list *list = NULL, *iter2;
+
+ commit = iter->item;
+ if (oidset_contains(&shown, &commit->object.oid))
+ continue;
+ entry = oidmap_get(&state.commit2label, &commit->object.oid);
+
+ if (entry)
+ fprintf(out, "\n# Branch %s\n", entry->string);
+ else
+ fprintf(out, "\n");
+
+ while (oidset_contains(&interesting, &commit->object.oid) &&
+ !oidset_contains(&shown, &commit->object.oid)) {
+ commit_list_insert(commit, &list);
+ if (!commit->parents) {
+ commit = NULL;
+ break;
+ }
+ commit = commit->parents->item;
+ }
+
+ if (!commit)
+ fprintf(out, "%s %s\n", cmd_reset,
+ rebase_cousins ? "onto" : "[new root]");
+ else {
+ const char *to = NULL;
+
+ entry = oidmap_get(&state.commit2label,
+ &commit->object.oid);
+ if (entry)
+ to = entry->string;
+ else if (!rebase_cousins)
+ to = label_oid(&commit->object.oid, NULL,
+ &state);
+
+ if (!to || !strcmp(to, "onto"))
+ fprintf(out, "%s onto\n", cmd_reset);
+ else {
+ strbuf_reset(&oneline);
+ pretty_print_commit(pp, commit, &oneline);
+ fprintf(out, "%s %s # %s\n",
+ cmd_reset, to, oneline.buf);
+ }
+ }
+
+ for (iter2 = list; iter2; iter2 = iter2->next) {
+ struct object_id *oid = &iter2->item->object.oid;
+ entry = oidmap_get(&commit2todo, oid);
+ /* only show if not already upstream */
+ if (entry)
+ fprintf(out, "%s\n", entry->string);
+ entry = oidmap_get(&state.commit2label, oid);
+ if (entry)
+ fprintf(out, "%s %s\n",
+ cmd_label, entry->string);
+ oidset_insert(&shown, oid);
+ }
+
+ free_commit_list(list);
+ }
+
+ free_commit_list(commits);
+ free_commit_list(tips);
+
+ strbuf_release(&label);
+ strbuf_release(&oneline);
+ strbuf_release(&buf);
+
+ oidmap_free(&commit2todo, 1);
+ oidmap_free(&state.commit2label, 1);
+ hashmap_free(&state.labels, 1);
+ strbuf_release(&state.buf);
+
+ return 0;
+}
+
int sequencer_make_script(FILE *out, int argc, const char **argv,
unsigned flags)
{
struct commit *commit;
int keep_empty = flags & TODO_LIST_KEEP_EMPTY;
const char *insn = flags & TODO_LIST_ABBREVIATE_CMDS ? "p" : "pick";
+ int rebase_merges = flags & TODO_LIST_REBASE_MERGES;
init_revisions(&revs, NULL);
revs.verbose_header = 1;
- revs.max_parents = 1;
+ if (!rebase_merges)
+ revs.max_parents = 1;
revs.cherry_mark = 1;
revs.limited = 1;
revs.reverse = 1;
if (prepare_revision_walk(&revs) < 0)
return error(_("make_script: error preparing revisions"));
+ if (rebase_merges)
+ return make_script_with_merges(&pp, &revs, out, flags);
+
while ((commit = get_revision(&revs))) {
int is_empty = is_original_commit_empty(commit);
short_commit_name(item->commit) :
oid_to_hex(&item->commit->object.oid);
+ if (item->command == TODO_MERGE) {
+ if (item->flags & TODO_EDIT_MERGE_MSG)
+ strbuf_addstr(&buf, " -c");
+ else
+ strbuf_addstr(&buf, " -C");
+ }
+
strbuf_addf(&buf, " %s", oid);
}
+
/* add all the rest */
if (!item->arg_len)
strbuf_addch(&buf, '\n');
oid = &item->commit->object.oid;
}
if (i > 0) {
- int offset = i < todo_list.nr ?
- todo_list.items[i].offset_in_buf : todo_list.buf.len;
+ int offset = get_item_line_offset(&todo_list, i);
const char *done_path = rebase_path_done();
fd = open(done_path, O_CREAT | O_WRONLY | O_APPEND, 0666);
struct subject2item_entry *entry;
next[i] = tail[i] = -1;
- if (item->command >= TODO_EXEC) {
+ if (!item->commit || item->command == TODO_DROP) {
subjects[i] = NULL;
continue;
}
continue;
while (cur >= 0) {
- int offset = todo_list.items[cur].offset_in_buf;
- int end_offset = cur + 1 < todo_list.nr ?
- todo_list.items[cur + 1].offset_in_buf :
- todo_list.buf.len;
- char *bol = todo_list.buf.buf + offset;
- char *eol = todo_list.buf.buf + end_offset;
+ const char *bol =
+ get_item_line(&todo_list, cur);
+ const char *eol =
+ get_item_line(&todo_list, cur + 1);
/* replace 'pick', by 'fixup' or 'squash' */
command = todo_list.items[cur].command;
char **xopts;
size_t xopts_nr, xopts_alloc;
+ /* Used by fixup/squash */
+ struct strbuf current_fixups;
+ int current_fixup_count;
+
+ /* placeholder commit for -i --root */
+ struct object_id squash_onto;
+ int have_squash_onto;
+
/* Only used by REPLAY_NONE */
struct rev_info *revs;
};
-#define REPLAY_OPTS_INIT { -1 }
+#define REPLAY_OPTS_INIT { .action = -1, .current_fixups = STRBUF_INIT }
/* Call this to setup defaults before parsing command line options */
void sequencer_init_config(struct replay_opts *opts);
#define TODO_LIST_KEEP_EMPTY (1U << 0)
#define TODO_LIST_SHORTEN_IDS (1U << 1)
#define TODO_LIST_ABBREVIATE_CMDS (1U << 2)
+#define TODO_LIST_REBASE_MERGES (1U << 3)
+/*
+ * When rebasing merges, commits that do have the base commit as ancestor
+ * ("cousins") are *not* rebased onto the new base by default. If those
+ * commits should be rebased onto the new base, this flag needs to be passed.
+ */
+#define TODO_LIST_REBASE_COUSINS (1U << 4)
int sequencer_make_script(FILE *out, int argc, const char **argv,
unsigned flags);
{ "agent", agent_advertise, NULL },
{ "ls-refs", always_advertise, ls_refs },
{ "fetch", upload_pack_advertise, upload_pack_v2 },
+ { "server-option", always_advertise, NULL },
};
static void advertise_capabilities(void)
int old_num;
int new_num;
int nr_alloc;
- int nr_heads;
- unsigned char (*head)[20];
} **info;
static int num_pack;
static const char *objdir;
else
stale = 1;
- for (i = 0; i < num_pack; i++) {
- if (stale) {
+ for (i = 0; i < num_pack; i++)
+ if (stale)
info[i]->old_num = -1;
- info[i]->nr_heads = 0;
- }
- }
/* renumber them */
QSORT(info, num_pack, compare_info);
case READ_GITFILE_ERR_NOT_A_REPO:
die(_("not a git repository: %s"), dir);
default:
- die("BUG: unknown error code");
+ BUG("unknown error code");
}
}
"Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set)."),
dir.buf);
default:
- die("BUG: unhandled setup_git_directory_1() result");
+ BUG("unhandled setup_git_directory_1() result");
}
if (prefix)
array->sorted = 0;
}
+
+int oid_array_for_each(struct oid_array *array,
+ for_each_oid_fn fn,
+ void *data)
+{
+ int i;
+
+ /* No oid_array_sort() here! See the api-oid-array.txt docs! */
+
+ for (i = 0; i < array->nr; i++) {
+ int ret = fn(array->oid + i, data);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
int oid_array_for_each_unique(struct oid_array *array,
- for_each_oid_fn fn,
- void *data)
+ for_each_oid_fn fn,
+ void *data)
{
int i;
typedef int (*for_each_oid_fn)(const struct object_id *oid,
void *data);
+int oid_array_for_each(struct oid_array *array,
+ for_each_oid_fn fn,
+ void *data);
int oid_array_for_each_unique(struct oid_array *array,
- for_each_oid_fn fn,
- void *data);
+ for_each_oid_fn fn,
+ void *data);
#endif /* SHA1_ARRAY_H */
/* The maximum size for an object header. */
#define MAX_HEADER_LEN 32
+
+#define EMPTY_TREE_SHA1_BIN_LITERAL \
+ "\x4b\x82\x5d\xc6\x42\xcb\x6e\xb9\xa0\x60" \
+ "\xe5\x4b\xf8\xd6\x92\x88\xfb\xee\x49\x04"
+
+#define EMPTY_BLOB_SHA1_BIN_LITERAL \
+ "\xe6\x9d\xe2\x9b\xb2\xd1\xd6\x43\x4b\x8b" \
+ "\x29\xae\x77\x5a\xd8\xc2\xe4\x8c\x53\x91"
+
const unsigned char null_sha1[GIT_MAX_RAWSZ];
const struct object_id null_oid;
-const struct object_id empty_tree_oid = {
+static const struct object_id empty_tree_oid = {
EMPTY_TREE_SHA1_BIN_LITERAL
};
-const struct object_id empty_blob_oid = {
+static const struct object_id empty_blob_oid = {
EMPTY_BLOB_SHA1_BIN_LITERAL
};
},
};
+const char *empty_tree_oid_hex(void)
+{
+ static char buf[GIT_MAX_HEXSZ + 1];
+ return oid_to_hex_r(buf, the_hash_algo->empty_tree);
+}
+
+const char *empty_blob_oid_hex(void)
+{
+ static char buf[GIT_MAX_HEXSZ + 1];
+ return oid_to_hex_r(buf, the_hash_algo->empty_blob);
+}
+
/*
* This is meant to hold a *small* number of objects that you would
* want read_sha1_file() to be able to return, but yet you do not want
* application).
*/
static struct cached_object {
- unsigned char sha1[20];
+ struct object_id oid;
enum object_type type;
void *buf;
unsigned long size;
static int cached_object_nr, cached_object_alloc;
static struct cached_object empty_tree = {
- EMPTY_TREE_SHA1_BIN_LITERAL,
+ { EMPTY_TREE_SHA1_BIN_LITERAL },
OBJ_TREE,
"",
0
};
-static struct cached_object *find_cached_object(const unsigned char *sha1)
+static struct cached_object *find_cached_object(const struct object_id *oid)
{
int i;
struct cached_object *co = cached_objects;
for (i = 0; i < cached_object_nr; i++, co++) {
- if (!hashcmp(co->sha1, sha1))
+ if (!oidcmp(&co->oid, oid))
return co;
}
- if (!hashcmp(sha1, empty_tree.sha1))
+ if (!oidcmp(oid, the_hash_algo->empty_tree))
return &empty_tree;
return NULL;
}
return 1;
}
-static int check_and_freshen_local(const unsigned char *sha1, int freshen)
+static int check_and_freshen_local(const struct object_id *oid, int freshen)
{
static struct strbuf buf = STRBUF_INIT;
strbuf_reset(&buf);
- sha1_file_name(the_repository, &buf, sha1);
+ sha1_file_name(the_repository, &buf, oid->hash);
return check_and_freshen_file(buf.buf, freshen);
}
-static int check_and_freshen_nonlocal(const unsigned char *sha1, int freshen)
+static int check_and_freshen_nonlocal(const struct object_id *oid, int freshen)
{
struct alternate_object_database *alt;
prepare_alt_odb(the_repository);
for (alt = the_repository->objects->alt_odb_list; alt; alt = alt->next) {
- const char *path = alt_sha1_path(alt, sha1);
+ const char *path = alt_sha1_path(alt, oid->hash);
if (check_and_freshen_file(path, freshen))
return 1;
}
return 0;
}
-static int check_and_freshen(const unsigned char *sha1, int freshen)
+static int check_and_freshen(const struct object_id *oid, int freshen)
{
- return check_and_freshen_local(sha1, freshen) ||
- check_and_freshen_nonlocal(sha1, freshen);
+ return check_and_freshen_local(oid, freshen) ||
+ check_and_freshen_nonlocal(oid, freshen);
}
-int has_loose_object_nonlocal(const unsigned char *sha1)
+int has_loose_object_nonlocal(const struct object_id *oid)
{
- return check_and_freshen_nonlocal(sha1, 0);
+ return check_and_freshen_nonlocal(oid, 0);
}
-static int has_loose_object(const unsigned char *sha1)
+static int has_loose_object(const struct object_id *oid)
{
- return check_and_freshen(sha1, 0);
+ return check_and_freshen(oid, 0);
}
static void mmap_limit_check(size_t length)
int fetch_if_missing = 1;
-int oid_object_info_extended(const struct object_id *oid, struct object_info *oi, unsigned flags)
+int oid_object_info_extended(struct repository *r, const struct object_id *oid,
+ struct object_info *oi, unsigned flags)
{
static struct object_info blank_oi = OBJECT_INFO_INIT;
struct pack_entry e;
int already_retried = 0;
if (flags & OBJECT_INFO_LOOKUP_REPLACE)
- real = lookup_replace_object(the_repository, oid);
+ real = lookup_replace_object(r, oid);
if (is_null_oid(real))
return -1;
oi = &blank_oi;
if (!(flags & OBJECT_INFO_SKIP_CACHED)) {
- struct cached_object *co = find_cached_object(real->hash);
+ struct cached_object *co = find_cached_object(real);
if (co) {
if (oi->typep)
*(oi->typep) = co->type;
}
while (1) {
- if (find_pack_entry(the_repository, real->hash, &e))
+ if (find_pack_entry(r, real, &e))
break;
if (flags & OBJECT_INFO_IGNORE_LOOSE)
return -1;
/* Most likely it's a loose object. */
- if (!sha1_loose_object_info(the_repository, real->hash, oi, flags))
+ if (!sha1_loose_object_info(r, real->hash, oi, flags))
return 0;
/* Not a loose object; someone else may have just packed it. */
if (!(flags & OBJECT_INFO_QUICK)) {
- reprepare_packed_git(the_repository);
- if (find_pack_entry(the_repository, real->hash, &e))
+ reprepare_packed_git(r);
+ if (find_pack_entry(r, real, &e))
break;
}
/* Check if it is a missing object */
if (fetch_if_missing && repository_format_partial_clone &&
- !already_retried) {
+ !already_retried && r == the_repository) {
/*
- * TODO Investigate haveing fetch_object() return
+ * TODO Investigate having fetch_object() return
* TODO error/success and stopping the music here.
+ * TODO Pass a repository struct through fetch_object,
+ * such that arbitrary repositories work.
*/
fetch_object(repository_format_partial_clone, real->hash);
already_retried = 1;
* information below, so return early.
*/
return 0;
- rtype = packed_object_info(e.p, e.offset, oi);
+ rtype = packed_object_info(r, e.p, e.offset, oi);
if (rtype < 0) {
mark_bad_packed_object(e.p, real->hash);
- return oid_object_info_extended(real, oi, 0);
+ return oid_object_info_extended(r, real, oi, 0);
} else if (oi->whence == OI_PACKED) {
oi->u.packed.offset = e.offset;
oi->u.packed.pack = e.p;
}
/* returns enum object_type or negative */
-int oid_object_info(const struct object_id *oid, unsigned long *sizep)
+int oid_object_info(struct repository *r,
+ const struct object_id *oid,
+ unsigned long *sizep)
{
enum object_type type;
struct object_info oi = OBJECT_INFO_INIT;
oi.typep = &type;
oi.sizep = sizep;
- if (oid_object_info_extended(oid, &oi,
- OBJECT_INFO_LOOKUP_REPLACE) < 0)
+ if (oid_object_info_extended(r, oid, &oi,
+ OBJECT_INFO_LOOKUP_REPLACE) < 0)
return -1;
return type;
}
hashcpy(oid.hash, sha1);
- if (oid_object_info_extended(&oid, &oi, 0) < 0)
+ if (oid_object_info_extended(the_repository, &oid, &oi, 0) < 0)
return NULL;
return content;
}
struct cached_object *co;
hash_object_file(buf, len, type_name(type), oid);
- if (has_sha1_file(oid->hash) || find_cached_object(oid->hash))
+ if (has_sha1_file(oid->hash) || find_cached_object(oid))
return 0;
ALLOC_GROW(cached_objects, cached_object_nr + 1, cached_object_alloc);
co = &cached_objects[cached_object_nr++];
co->type = type;
co->buf = xmalloc(len);
memcpy(co->buf, buf, len);
- hashcpy(co->sha1, oid->hash);
+ oidcpy(&co->oid, oid);
return 0;
}
return finalize_object_file(tmp_file.buf, filename.buf);
}
-static int freshen_loose_object(const unsigned char *sha1)
+static int freshen_loose_object(const struct object_id *oid)
{
- return check_and_freshen(sha1, 1);
+ return check_and_freshen(oid, 1);
}
-static int freshen_packed_object(const unsigned char *sha1)
+static int freshen_packed_object(const struct object_id *oid)
{
struct pack_entry e;
- if (!find_pack_entry(the_repository, sha1, &e))
+ if (!find_pack_entry(the_repository, oid, &e))
return 0;
if (e.p->freshened)
return 1;
* it out into .git/objects/??/?{38} file.
*/
write_object_file_prepare(buf, len, type, oid, hdr, &hdrlen);
- if (freshen_packed_object(oid->hash) || freshen_loose_object(oid->hash))
+ if (freshen_packed_object(oid) || freshen_loose_object(oid))
return 0;
return write_loose_object(oid, hdr, hdrlen, buf, len, 0);
}
if (!(flags & HASH_WRITE_OBJECT))
goto cleanup;
- if (freshen_packed_object(oid->hash) || freshen_loose_object(oid->hash))
+ if (freshen_packed_object(oid) || freshen_loose_object(oid))
goto cleanup;
status = write_loose_object(oid, header, hdrlen, buf, len, 0);
int hdrlen;
int ret;
- if (has_loose_object(oid->hash))
+ if (has_loose_object(oid))
return 0;
buf = read_object(oid->hash, &type, &len);
if (!buf)
if (!startup_info->have_repository)
return 0;
hashcpy(oid.hash, sha1);
- return oid_object_info_extended(&oid, NULL,
+ return oid_object_info_extended(the_repository, &oid, NULL,
flags | OBJECT_INFO_SKIP_CACHED) >= 0;
}
void assert_oid_type(const struct object_id *oid, enum object_type expect)
{
- enum object_type type = oid_object_info(oid, NULL);
+ enum object_type type = oid_object_info(the_repository, oid, NULL);
if (type < 0)
die("%s is not a valid object", oid_to_hex(oid));
if (type != expect)
goto out;
}
- if (*type == OBJ_BLOB) {
+ if (*type == OBJ_BLOB && *size > big_file_threshold) {
if (check_stream_sha1(&stream, hdr, *size, path, expected_oid->hash) < 0)
goto out;
} else {
mi = (nr - 1) * (miv - lov) / (hiv - lov);
if (lo <= mi && mi < hi)
break;
- die("BUG: assertion failed in binary search");
+ BUG("assertion failed in binary search");
}
}
}
static int disambiguate_commit_only(const struct object_id *oid, void *cb_data_unused)
{
- int kind = oid_object_info(oid, NULL);
+ int kind = oid_object_info(the_repository, oid, NULL);
return kind == OBJ_COMMIT;
}
struct object *obj;
int kind;
- kind = oid_object_info(oid, NULL);
+ kind = oid_object_info(the_repository, oid, NULL);
if (kind == OBJ_COMMIT)
return 1;
if (kind != OBJ_TAG)
static int disambiguate_tree_only(const struct object_id *oid, void *cb_data_unused)
{
- int kind = oid_object_info(oid, NULL);
+ int kind = oid_object_info(the_repository, oid, NULL);
return kind == OBJ_TREE;
}
struct object *obj;
int kind;
- kind = oid_object_info(oid, NULL);
+ kind = oid_object_info(the_repository, oid, NULL);
if (kind == OBJ_TREE || kind == OBJ_COMMIT)
return 1;
if (kind != OBJ_TAG)
static int disambiguate_blob_only(const struct object_id *oid, void *cb_data_unused)
{
- int kind = oid_object_info(oid, NULL);
+ int kind = oid_object_info(the_repository, oid, NULL);
return kind == OBJ_BLOB;
}
struct strbuf desc = STRBUF_INIT;
int type;
-
if (ds->fn && !ds->fn(oid, ds->cb_data))
return 0;
- type = oid_object_info(oid, NULL);
+ type = oid_object_info(the_repository, oid, NULL);
if (type == OBJ_COMMIT) {
struct commit *commit = lookup_commit(oid);
if (commit) {
return 0;
}
+static int collect_ambiguous(const struct object_id *oid, void *data)
+{
+ oid_array_append(data, oid);
+ return 0;
+}
+
+static int sort_ambiguous(const void *a, const void *b)
+{
+ int a_type = oid_object_info(the_repository, a, NULL);
+ int b_type = oid_object_info(the_repository, b, NULL);
+ int a_type_sort;
+ int b_type_sort;
+
+ /*
+ * Sorts by hash within the same object type, just as
+ * oid_array_for_each_unique() would do.
+ */
+ if (a_type == b_type)
+ return oidcmp(a, b);
+
+ /*
+ * Between object types show tags, then commits, and finally
+ * trees and blobs.
+ *
+ * The object_type enum is commit, tree, blob, tag, but we
+ * want tag, commit, tree blob. Cleverly (perhaps too
+ * cleverly) do that with modulus, since the enum assigns 1 to
+ * commit, so tag becomes 0.
+ */
+ a_type_sort = a_type % 4;
+ b_type_sort = b_type % 4;
+ return a_type_sort > b_type_sort ? 1 : -1;
+}
+
static int get_short_oid(const char *name, int len, struct object_id *oid,
unsigned flags)
{
return -1;
if (HAS_MULTI_BITS(flags & GET_OID_DISAMBIGUATORS))
- die("BUG: multiple get_short_oid disambiguator flags");
+ BUG("multiple get_short_oid disambiguator flags");
if (flags & GET_OID_COMMIT)
ds.fn = disambiguate_commit_only;
status = finish_object_disambiguation(&ds, oid);
if (!quietly && (status == SHORT_NAME_AMBIGUOUS)) {
+ struct oid_array collect = OID_ARRAY_INIT;
+
error(_("short SHA1 %s is ambiguous"), ds.hex_pfx);
/*
ds.fn = NULL;
advise(_("The candidates are:"));
- for_each_abbrev(ds.hex_pfx, show_ambiguous_object, &ds);
+ for_each_abbrev(ds.hex_pfx, collect_ambiguous, &collect);
+ QSORT(collect.oid, collect.nr, sort_ambiguous);
+
+ if (oid_array_for_each(&collect, show_ambiguous_object, &ds))
+ BUG("show_ambiguous_object shouldn't return non-zero");
+ oid_array_clear(&collect);
}
return status;
}
-static int collect_ambiguous(const struct object_id *oid, void *data)
-{
- oid_array_append(data, oid);
- return 0;
-}
-
int for_each_abbrev(const char *prefix, each_abbrev_fn fn, void *cb_data)
{
struct oid_array collect = OID_ARRAY_INIT;
if (o->type == OBJ_TAG)
o = ((struct tag*) o)->tagged;
else if (o->type == OBJ_COMMIT)
- o = &(((struct commit *) o)->tree->object);
+ o = &(get_commit_tree(((struct commit *)o))->object);
else {
if (name)
error("%.*s: expected %s type, but the object "
if (new_filename)
filename = new_filename;
if (flags & GET_OID_FOLLOW_SYMLINKS) {
- ret = get_tree_entry_follow_symlinks(tree_oid.hash,
- filename, oid->hash, &oc->symlink_path,
+ ret = get_tree_entry_follow_symlinks(&tree_oid,
+ filename, oid, &oc->symlink_path,
&oc->mode);
} else {
ret = get_tree_entry(&tree_oid, filename, oid,
name, len);
}
}
- hashcpy(oc->tree, tree_oid.hash);
if (flags & GET_OID_RECORD_PATH)
oc->path = xstrdup(filename);
int get_oid_with_context(const char *str, unsigned flags, struct object_id *oid, struct object_context *oc)
{
if (flags & GET_OID_FOLLOW_SYMLINKS && flags & GET_OID_ONLY_TO_DIE)
- die("BUG: incompatible flags for get_sha1_with_context");
+ BUG("incompatible flags for get_sha1_with_context");
return get_oid_with_context_1(str, flags, NULL, oid, oc);
}
void set_alternate_shallow_file(const char *path, int override)
{
if (is_shallow != -1)
- die("BUG: is_repository_shallow must not be called before set_alternate_shallow_file");
+ BUG("is_repository_shallow must not be called before set_alternate_shallow_file");
if (alternate_shallow_file && !override)
return;
free(alternate_shallow_file);
static void check_shallow_file_for_update(void)
{
if (is_shallow == -1)
- die("BUG: shallow must be initialized by now");
+ BUG("shallow must be initialized by now");
if (!stat_validity_check(&shallow_stat, git_path_shallow()))
die("shallow file has changed since we read it");
*/
void prune_shallow(int show_only)
{
- static struct lock_file shallow_lock;
+ struct lock_file shallow_lock = LOCK_INIT;
struct strbuf sb = STRBUF_INIT;
int fd;
void *p;
if (!info->pool_count || size > info->end - info->free) {
if (size > POOL_SIZE)
- die("BUG: pool size too small for %d in paint_alloc()",
+ BUG("pool size too small for %d in paint_alloc()",
size);
info->pool_count++;
REALLOC_ARRAY(info->pools, info->pool_count);
static void check_signum(int sig)
{
if (sig < 1 || sig >= SIGCHAIN_MAX_SIGNALS)
- die("BUG: signal out of range: %d", sig);
+ BUG("signal out of range: %d", sig);
}
int sigchain_push(int sig, sigchain_fun f)
struct split_index *si;
int ret;
- if (sz < 20)
+ if (sz < the_hash_algo->rawsz)
return error("corrupt link extension (too short)");
si = init_split_index(istate);
- hashcpy(si->base_sha1, data);
- data += 20;
- sz -= 20;
+ hashcpy(si->base_oid.hash, data);
+ data += the_hash_algo->rawsz;
+ sz -= the_hash_algo->rawsz;
if (!sz)
return 0;
si->delete_bitmap = ewah_new();
struct index_state *istate)
{
struct split_index *si = istate->split_index;
- strbuf_add(sb, si->base_sha1, 20);
+ strbuf_add(sb, si->base_oid.hash, the_hash_algo->rawsz);
if (!si->delete_bitmap && !si->replace_bitmap)
return 0;
ewah_serialize_strbuf(si->delete_bitmap, sb);
#ifndef SPLIT_INDEX_H
#define SPLIT_INDEX_H
+#include "cache.h"
+
struct index_state;
struct strbuf;
struct ewah_bitmap;
struct split_index {
- unsigned char base_sha1[20];
+ struct object_id base_oid;
struct index_state *base;
struct ewah_bitmap *delete_bitmap;
struct ewah_bitmap *replace_bitmap;
#include "cache.h"
#include "refs.h"
+#include "string-list.h"
#include "utf8.h"
int starts_with(const char *str, const char *prefix)
return ret;
}
+void strbuf_add_separated_string_list(struct strbuf *str,
+ const char *sep,
+ struct string_list *slist)
+{
+ struct string_list_item *item;
+ int sep_needed = 0;
+
+ for_each_string_list_item(item, slist) {
+ if (sep_needed)
+ strbuf_addstr(str, sep);
+ strbuf_addstr(str, item->string);
+ sep_needed = 1;
+ }
+}
+
void strbuf_list_free(struct strbuf **sbs)
{
struct strbuf **s = sbs;
len = vsnprintf(sb->buf + sb->len, sb->alloc - sb->len, fmt, cp);
va_end(cp);
if (len < 0)
- die("BUG: your vsnprintf is broken (returned %d)", len);
+ BUG("your vsnprintf is broken (returned %d)", len);
if (len > strbuf_avail(sb)) {
strbuf_grow(sb, len);
len = vsnprintf(sb->buf + sb->len, sb->alloc - sb->len, fmt, ap);
if (len > strbuf_avail(sb))
- die("BUG: your vsnprintf is broken (insatiable)");
+ BUG("your vsnprintf is broken (insatiable)");
}
strbuf_setlen(sb, sb->len + len);
}
#ifndef STRBUF_H
#define STRBUF_H
+struct string_list;
+
/**
* strbuf's are meant to be used with all the usual C string and memory
* APIs. Given that the length of the buffer is known, it's often better to
return strbuf_split_max(sb, terminator, 0);
}
+/*
+ * Adds all strings of a string list to the strbuf, separated by the given
+ * separator. For example, if sep is
+ * ', '
+ * and slist contains
+ * ['element1', 'element2', ..., 'elementN'],
+ * then write:
+ * 'element1, element2, ..., elementN'
+ * to str. If only one element, just write "element1" to str.
+ */
+extern void strbuf_add_separated_string_list(struct strbuf *str,
+ const char *sep,
+ struct string_list *slist);
+
/**
* Free a NULL-terminated list of strbufs (for example, the return
* values of the strbuf_split*() functions).
oi->typep = type;
oi->sizep = &size;
- status = oid_object_info_extended(oid, oi, 0);
+ status = oid_object_info_extended(the_repository, oid, oi, 0);
if (status < 0)
return stream_error;
const struct submodule_entry *b = entry_or_key;
return strcmp(a->config->path, b->config->path) ||
- hashcmp(a->config->gitmodules_sha1, b->config->gitmodules_sha1);
+ oidcmp(&a->config->gitmodules_oid, &b->config->gitmodules_oid);
}
static int config_name_cmp(const void *unused_cmp_data,
const struct submodule_entry *b = entry_or_key;
return strcmp(a->config->name, b->config->name) ||
- hashcmp(a->config->gitmodules_sha1, b->config->gitmodules_sha1);
+ oidcmp(&a->config->gitmodules_oid, &b->config->gitmodules_oid);
}
static struct submodule_cache *submodule_cache_alloc(void)
free(cache);
}
-static unsigned int hash_sha1_string(const unsigned char *sha1,
- const char *string)
+static unsigned int hash_oid_string(const struct object_id *oid,
+ const char *string)
{
- return memhash(sha1, 20) + strhash(string);
+ return memhash(oid->hash, the_hash_algo->rawsz) + strhash(string);
}
static void cache_put_path(struct submodule_cache *cache,
struct submodule *submodule)
{
- unsigned int hash = hash_sha1_string(submodule->gitmodules_sha1,
- submodule->path);
+ unsigned int hash = hash_oid_string(&submodule->gitmodules_oid,
+ submodule->path);
struct submodule_entry *e = xmalloc(sizeof(*e));
hashmap_entry_init(e, hash);
e->config = submodule;
static void cache_remove_path(struct submodule_cache *cache,
struct submodule *submodule)
{
- unsigned int hash = hash_sha1_string(submodule->gitmodules_sha1,
- submodule->path);
+ unsigned int hash = hash_oid_string(&submodule->gitmodules_oid,
+ submodule->path);
struct submodule_entry e;
struct submodule_entry *removed;
hashmap_entry_init(&e, hash);
static void cache_add(struct submodule_cache *cache,
struct submodule *submodule)
{
- unsigned int hash = hash_sha1_string(submodule->gitmodules_sha1,
- submodule->name);
+ unsigned int hash = hash_oid_string(&submodule->gitmodules_oid,
+ submodule->name);
struct submodule_entry *e = xmalloc(sizeof(*e));
hashmap_entry_init(e, hash);
e->config = submodule;
}
static const struct submodule *cache_lookup_path(struct submodule_cache *cache,
- const unsigned char *gitmodules_sha1, const char *path)
+ const struct object_id *gitmodules_oid, const char *path)
{
struct submodule_entry *entry;
- unsigned int hash = hash_sha1_string(gitmodules_sha1, path);
+ unsigned int hash = hash_oid_string(gitmodules_oid, path);
struct submodule_entry key;
struct submodule key_config;
- hashcpy(key_config.gitmodules_sha1, gitmodules_sha1);
+ oidcpy(&key_config.gitmodules_oid, gitmodules_oid);
key_config.path = path;
hashmap_entry_init(&key, hash);
}
static struct submodule *cache_lookup_name(struct submodule_cache *cache,
- const unsigned char *gitmodules_sha1, const char *name)
+ const struct object_id *gitmodules_oid, const char *name)
{
struct submodule_entry *entry;
- unsigned int hash = hash_sha1_string(gitmodules_sha1, name);
+ unsigned int hash = hash_oid_string(gitmodules_oid, name);
struct submodule_entry key;
struct submodule key_config;
- hashcpy(key_config.gitmodules_sha1, gitmodules_sha1);
+ oidcpy(&key_config.gitmodules_oid, gitmodules_oid);
key_config.name = name;
hashmap_entry_init(&key, hash);
return NULL;
}
+int check_submodule_name(const char *name)
+{
+ /* Disallow empty names */
+ if (!*name)
+ return -1;
+
+ /*
+ * Look for '..' as a path component. Check both '/' and '\\' as
+ * separators rather than is_dir_sep(), because we want the name rules
+ * to be consistent across platforms.
+ */
+ goto in_component; /* always start inside component */
+ while (*name) {
+ char c = *name++;
+ if (c == '/' || c == '\\') {
+in_component:
+ if (name[0] == '.' && name[1] == '.' &&
+ (!name[2] || name[2] == '/' || name[2] == '\\'))
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
static int name_and_item_from_var(const char *var, struct strbuf *name,
struct strbuf *item)
{
return 0;
strbuf_add(name, subsection, subsection_len);
+ if (check_submodule_name(name->buf) < 0) {
+ warning(_("ignoring suspicious submodule name: %s"), name->buf);
+ strbuf_release(name);
+ return 0;
+ }
+
strbuf_addstr(item, key);
return 1;
}
static struct submodule *lookup_or_create_by_name(struct submodule_cache *cache,
- const unsigned char *gitmodules_sha1, const char *name)
+ const struct object_id *gitmodules_oid, const char *name)
{
struct submodule *submodule;
struct strbuf name_buf = STRBUF_INIT;
- submodule = cache_lookup_name(cache, gitmodules_sha1, name);
+ submodule = cache_lookup_name(cache, gitmodules_oid, name);
if (submodule)
return submodule;
submodule->branch = NULL;
submodule->recommend_shallow = -1;
- hashcpy(submodule->gitmodules_sha1, gitmodules_sha1);
+ oidcpy(&submodule->gitmodules_oid, gitmodules_oid);
cache_add(cache, submodule);
return parse_push_recurse(opt, arg, 1);
}
-static void warn_multiple_config(const unsigned char *treeish_name,
+static void warn_multiple_config(const struct object_id *treeish_name,
const char *name, const char *option)
{
const char *commit_string = "WORKTREE";
if (treeish_name)
- commit_string = sha1_to_hex(treeish_name);
+ commit_string = oid_to_hex(treeish_name);
warning("%s:.gitmodules, multiple configurations found for "
"'submodule.%s.%s'. Skipping second one!",
commit_string, name, option);
struct parse_config_parameter {
struct submodule_cache *cache;
- const unsigned char *treeish_name;
- const unsigned char *gitmodules_sha1;
+ const struct object_id *treeish_name;
+ const struct object_id *gitmodules_oid;
int overwrite;
};
return 0;
submodule = lookup_or_create_by_name(me->cache,
- me->gitmodules_sha1,
+ me->gitmodules_oid,
name.buf);
if (!strcmp(item.buf, "path")) {
}
} else if (!strcmp(item.buf, "fetchrecursesubmodules")) {
/* when parsing worktree configurations we can die early */
- int die_on_error = is_null_sha1(me->gitmodules_sha1);
+ int die_on_error = is_null_oid(me->gitmodules_oid);
if (!me->overwrite &&
submodule->fetch_recurse != RECURSE_SUBMODULES_NONE)
warn_multiple_config(me->treeish_name, submodule->name,
switch (lookup_type) {
case lookup_name:
- submodule = cache_lookup_name(cache, oid.hash, key);
+ submodule = cache_lookup_name(cache, &oid, key);
break;
case lookup_path:
- submodule = cache_lookup_path(cache, oid.hash, key);
+ submodule = cache_lookup_path(cache, &oid, key);
break;
}
if (submodule)
/* fill the submodule config into the cache */
parameter.cache = cache;
- parameter.treeish_name = treeish_name->hash;
- parameter.gitmodules_sha1 = oid.hash;
+ parameter.treeish_name = treeish_name;
+ parameter.gitmodules_oid = &oid;
parameter.overwrite = 0;
git_config_from_mem(parse_config, CONFIG_ORIGIN_SUBMODULE_BLOB, rev.buf,
config, config_size, ¶meter);
switch (lookup_type) {
case lookup_name:
- return cache_lookup_name(cache, oid.hash, key);
+ return cache_lookup_name(cache, &oid, key);
case lookup_path:
- return cache_lookup_path(cache, oid.hash, key);
+ return cache_lookup_path(cache, &oid, key);
default:
return NULL;
}
parameter.cache = repo->submodule_cache;
parameter.treeish_name = NULL;
- parameter.gitmodules_sha1 = null_sha1;
+ parameter.gitmodules_oid = &null_oid;
parameter.overwrite = 1;
return parse_config(var, value, ¶meter);
#ifndef SUBMODULE_CONFIG_CACHE_H
#define SUBMODULE_CONFIG_CACHE_H
+#include "cache.h"
#include "hashmap.h"
#include "submodule.h"
#include "strbuf.h"
const char *ignore;
const char *branch;
struct submodule_update_strategy update_strategy;
- /* the sha1 blob id of the responsible .gitmodules file */
- unsigned char gitmodules_sha1[20];
+ /* the object id of the responsible .gitmodules file */
+ struct object_id gitmodules_oid;
int recommend_shallow;
};
#define SUBMODULE_INIT { NULL, NULL, NULL, RECURSE_SUBMODULES_NONE, \
- NULL, NULL, SUBMODULE_UPDATE_STRATEGY_INIT, {0}, -1 };
+ NULL, NULL, SUBMODULE_UPDATE_STRATEGY_INIT, { { 0 } }, -1 };
struct submodule_cache;
struct repository;
const char *path);
void submodule_free(struct repository *r);
+/*
+ * Returns 0 if the name is syntactically acceptable as a submodule "name"
+ * (e.g., that may be found in the subsection of a .gitmodules file) and -1
+ * otherwise.
+ */
+int check_submodule_name(const char *name);
+
#endif /* SUBMODULE_CONFIG_H */
{
struct has_commit_data *cb = data;
- enum object_type type = oid_object_info(oid, NULL);
+ enum object_type type = oid_object_info(the_repository, oid, NULL);
switch (type) {
case OBJ_COMMIT:
static int push_submodule(const char *path,
const struct remote *remote,
- const char **refspec, int refspec_nr,
+ const struct refspec *rs,
const struct string_list *push_options,
int dry_run)
{
if (remote->origin != REMOTE_UNCONFIGURED) {
int i;
argv_array_push(&cp.args, remote->name);
- for (i = 0; i < refspec_nr; i++)
- argv_array_push(&cp.args, refspec[i]);
+ for (i = 0; i < rs->raw_nr; i++)
+ argv_array_push(&cp.args, rs->raw[i]);
}
prepare_submodule_repo_env(&cp.env_array);
*/
static void submodule_push_check(const char *path, const char *head,
const struct remote *remote,
- const char **refspec, int refspec_nr)
+ const struct refspec *rs)
{
struct child_process cp = CHILD_PROCESS_INIT;
int i;
argv_array_push(&cp.args, head);
argv_array_push(&cp.args, remote->name);
- for (i = 0; i < refspec_nr; i++)
- argv_array_push(&cp.args, refspec[i]);
+ for (i = 0; i < rs->raw_nr; i++)
+ argv_array_push(&cp.args, rs->raw[i]);
prepare_submodule_repo_env(&cp.env_array);
cp.git_cmd = 1;
int push_unpushed_submodules(struct oid_array *commits,
const struct remote *remote,
- const char **refspec, int refspec_nr,
+ const struct refspec *rs,
const struct string_list *push_options,
int dry_run)
{
for (i = 0; i < needs_pushing.nr; i++)
submodule_push_check(needs_pushing.items[i].string,
- head, remote,
- refspec, refspec_nr);
+ head, remote, rs);
free(head);
}
for (i = 0; i < needs_pushing.nr; i++) {
const char *path = needs_pushing.items[i].string;
fprintf(stderr, "Pushing submodule '%s'\n", path);
- if (!push_submodule(path, remote, refspec, refspec_nr,
+ if (!push_submodule(path, remote, rs,
push_options, dry_run)) {
fprintf(stderr, "Unable to push submodule '%s'\n", path);
ret = 0;
buf.buf[0] == '2') {
/* T = line type, XY = status, SSSS = submodule state */
if (buf.len < strlen("T XY SSSS"))
- die("BUG: invalid status --porcelain=2 line %s",
+ BUG("invalid status --porcelain=2 line %s",
buf.buf);
if (buf.buf[5] == 'S' && buf.buf[8] == 'U')
get_super_prefix_or_empty(), path);
argv_array_pushl(&cp.args, "read-tree", "-u", "--reset", NULL);
- argv_array_push(&cp.args, EMPTY_TREE_SHA1_HEX);
+ argv_array_push(&cp.args, empty_tree_oid_hex());
if (run_command(&cp))
die("could not reset submodule index");
sub = submodule_from_path(the_repository, &null_oid, path);
if (!sub)
- die("BUG: could not get submodule information for '%s'", path);
+ BUG("could not get submodule information for '%s'", path);
if (old_head && !(flags & SUBMODULE_MOVE_HEAD_FORCE)) {
/* Check if the submodule has a dirty index. */
argv_array_push(&cp.args, "-m");
if (!(flags & SUBMODULE_MOVE_HEAD_FORCE))
- argv_array_push(&cp.args, old_head ? old_head : EMPTY_TREE_SHA1_HEX);
+ argv_array_push(&cp.args, old_head ? old_head : empty_tree_oid_hex());
- argv_array_push(&cp.args, new_head ? new_head : EMPTY_TREE_SHA1_HEX);
+ argv_array_push(&cp.args, new_head ? new_head : empty_tree_oid_hex());
if (run_command(&cp)) {
ret = -1;
struct strbuf sb = STRBUF_INIT;
if (flags & ~ABSORB_GITDIR_RECURSE_SUBMODULES)
- die("BUG: we don't know how to pass the flags down?");
+ BUG("we don't know how to pass the flags down?");
strbuf_addstr(&sb, get_super_prefix_or_empty());
strbuf_addstr(&sb, path);
if (super_sub_len > cwd_len ||
strcmp(&cwd[cwd_len - super_sub_len], super_sub))
- die (_("BUG: returned path string doesn't match cwd?"));
+ BUG("returned path string doesn't match cwd?");
super_wt = xstrdup(cwd);
super_wt[cwd_len - super_sub_len] = '\0';
extern int find_unpushed_submodules(struct oid_array *commits,
const char *remotes_name,
struct string_list *needs_pushing);
+struct refspec;
extern int push_unpushed_submodules(struct oid_array *commits,
const struct remote *remote,
- const char **refspec, int refspec_nr,
+ const struct refspec *rs,
const struct string_list *push_options,
int dry_run);
/*
everything up to a certain test.
+Running tests with special setups
+---------------------------------
+
+The whole test suite could be run to test some special features
+that cannot be easily covered by a few specific test cases. These
+could be enabled by running the test suite with correct GIT_TEST_
+environment set.
+
+GIT_TEST_SPLIT_INDEX=<boolean> forces split-index mode on the whole
+test suite. Accept any boolean values that are accepted by git-config.
+
+GIT_TEST_FULL_IN_PACK_ARRAY=<boolean> exercises the uncommon
+pack-objects code path where there are more than 1024 packs even if
+the actual number of packs in repository is below this limit. Accept
+any boolean values that are accepted by git-config.
+
+GIT_TEST_OE_SIZE=<n> exercises the uncommon pack-objects code path
+where we do not cache object size in memory and read it from existing
+packs on demand. This normally only happens when the object size is
+over 2GB. This variable forces the code path on any object larger than
+<n> bytes.
+
Naming Tests
------------
#include "git-compat-util.h"
#if defined(GIT_WINDOWS_NATIVE)
+#include "lazyload.h"
static int cmd_sync(void)
{
{
HANDLE hProcess = GetCurrentProcess();
HANDLE hToken;
- HMODULE ntdll;
- DWORD(WINAPI *NtSetSystemInformation)(INT, PVOID, ULONG);
+ DECLARE_PROC_ADDR(ntdll.dll, DWORD, NtSetSystemInformation, INT, PVOID, ULONG);
SYSTEM_MEMORY_LIST_COMMAND command;
int status;
CloseHandle(hToken);
- ntdll = LoadLibrary("ntdll.dll");
- if (!ntdll)
- return error("Can't load ntdll.dll, wrong Windows version?");
-
- NtSetSystemInformation =
- (DWORD(WINAPI *)(INT, PVOID, ULONG))GetProcAddress(ntdll, "NtSetSystemInformation");
- if (!NtSetSystemInformation)
- return error("Can't get function addresses, wrong Windows version?");
+ if (!INIT_PROC_ADDR(NtSetSystemInformation))
+ return error("Could not find NtSetSystemInformation() function");
command = MemoryPurgeStandbyList;
status = NtSetSystemInformation(
else if (status != STATUS_SUCCESS)
error("Unable to execute the memory list command %d", status);
- FreeLibrary(ntdll);
-
return status;
}
int i;
do_read_index(&the_index, av[1], 1);
- printf("own %s\n", sha1_to_hex(the_index.sha1));
+ printf("own %s\n", oid_to_hex(&the_index.oid));
si = the_index.split_index;
if (!si) {
printf("not a split index\n");
return 0;
}
- printf("base %s\n", sha1_to_hex(si->base_sha1));
+ printf("base %s\n", oid_to_hex(&si->base_oid));
for (i = 0; i < the_index.cache_nr; i++) {
struct cache_entry *ce = the_index.cache[i];
printf("%06o %s %d\t%s\n", ce->ce_mode,
len = base->len;
strbuf_addf(base, "%s/", ucd->name);
printf("%s %s", base->buf,
- sha1_to_hex(ucd->exclude_sha1));
+ oid_to_hex(&ucd->exclude_oid));
if (ucd->recurse)
fputs(" recurse", stdout);
if (ucd->check_only)
two = lookup_unknown_object(two_oid.hash);
ret = add_decoration(&n, one, &decoration_a);
if (ret)
- die("BUG: when adding a brand-new object, NULL should be returned");
+ BUG("when adding a brand-new object, NULL should be returned");
ret = add_decoration(&n, two, NULL);
if (ret)
- die("BUG: when adding a brand-new object, NULL should be returned");
+ BUG("when adding a brand-new object, NULL should be returned");
/*
* When re-adding an already existing object, the old decoration is
*/
ret = add_decoration(&n, one, NULL);
if (ret != &decoration_a)
- die("BUG: when readding an already existing object, existing decoration should be returned");
+ BUG("when readding an already existing object, existing decoration should be returned");
ret = add_decoration(&n, two, &decoration_b);
if (ret)
- die("BUG: when readding an already existing object, existing decoration should be returned");
+ BUG("when readding an already existing object, existing decoration should be returned");
/*
* Lookup returns the added declarations, or NULL if the object was
*/
ret = lookup_decoration(&n, one);
if (ret)
- die("BUG: lookup should return added declaration");
+ BUG("lookup should return added declaration");
ret = lookup_decoration(&n, two);
if (ret != &decoration_b)
- die("BUG: lookup should return added declaration");
+ BUG("lookup should return added declaration");
three = lookup_unknown_object(three_oid.hash);
ret = lookup_decoration(&n, three);
if (ret)
- die("BUG: lookup for unknown object should return NULL");
+ BUG("lookup for unknown object should return NULL");
/*
* The user can also loop through all entries.
objects_noticed++;
}
if (objects_noticed != 2)
- die("BUG: should have 2 objects");
+ BUG("should have 2 objects");
return 0;
}
#include "test-tool.h"
#include "cache.h"
#include "string-list.h"
+#include "utf8.h"
/*
* A "string_list_each_func_t" function that normalizes an entry from
{ NULL, NULL }
};
+static int is_dotgitmodules(const char *path)
+{
+ return is_hfs_dotgitmodules(path) || is_ntfs_dotgitmodules(path);
+}
+
int cmd__path_utils(int argc, const char **argv)
{
if (argc == 3 && !strcmp(argv[1], "normalize_path_copy")) {
if (argc == 2 && !strcmp(argv[1], "dirname"))
return test_function(dirname_data, posix_dirname, argv[1]);
+ if (argc > 2 && !strcmp(argv[1], "is_dotgitmodules")) {
+ int res = 0, expect = 1, i;
+ for (i = 2; i < argc; i++)
+ if (!strcmp("--not", argv[i]))
+ expect = !expect;
+ else if (expect != is_dotgitmodules(argv[i]))
+ res = error("'%s' is %s.gitmodules", argv[i],
+ expect ? "not " : "");
+ else
+ fprintf(stderr, "ok: '%s' is %s.gitmodules\n",
+ argv[i], expect ? "" : "not ");
+ return !!res;
+ }
+
fprintf(stderr, "%s: unknown function name: %s\n", argv[0],
argv[1] ? argv[1] : "(there was none)");
return 1;
#include "tree.h"
#include "cache-tree.h"
-static struct lock_file index_lock;
-
int cmd__scrap_cache_tree(int ac, const char **av)
{
+ struct lock_file index_lock = LOCK_INIT;
+
setup_git_directory();
hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR);
if (read_cache() < 0)
{
int i;
+ BUG_exit_code = 99;
if (argc < 2)
die("I need a test name!");
#include "cache.h"
#include "lockfile.h"
-static struct lock_file index_lock;
-
int cmd__write_cache(int argc, const char **argv)
{
- int i, cnt = 1, lockfd;
+ struct lock_file index_lock = LOCK_INIT;
+ int i, cnt = 1;
if (argc == 2)
cnt = strtol(argv[1], NULL, 0);
setup_git_directory();
read_cache();
for (i = 0; i < cnt; i++) {
- lockfd = hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR);
- if (0 <= lockfd) {
- write_locked_index(&the_index, &index_lock, COMMIT_LOCK);
- } else {
- rollback_lock_file(&index_lock);
- }
+ hold_locked_index(&index_lock, LOCK_DIE_ON_ERROR);
+ if (write_locked_index(&the_index, &index_lock, COMMIT_LOCK))
+ die("unable to write index file");
}
return 0;
;;
esac
+ # If it's not a delta, we can convince pack-objects to generate a pack
+ # with just our entry, and then strip off the header (12 bytes) and
+ # trailer (20 bytes).
+ if test -z "$2"
+ then
+ echo "$1" | git pack-objects --stdout >pack_obj.tmp &&
+ size=$(wc -c <pack_obj.tmp) &&
+ dd if=pack_obj.tmp bs=1 count=$((size - 20 - 12)) skip=12 &&
+ rm -f pack_obj.tmp
+ return
+ fi
+
echo >&2 "BUG: don't know how to print $1${2:+ (from $2)}"
return 1
}
use strict;
use warnings;
use JSON;
+use Getopt::Long;
use Git;
sub get_times {
return $out;
}
+sub usage {
+ print <<EOT;
+./aggregate.perl [options] [--] [<dir_or_rev>...] [--] [<test_script>...] >
+
+ Options:
+ --codespeed * Format output for Codespeed
+ --reponame <str> * Send given reponame to codespeed
+ --sort-by <str> * Sort output (only "regression" criteria is supported)
+ --subsection <str> * Use results from given subsection
+
+EOT
+ exit(1);
+}
+
my (@dirs, %dirnames, %dirabbrevs, %prefixes, @tests,
$codespeed, $sortby, $subsection, $reponame);
+
+Getopt::Long::Configure qw/ require_order /;
+
+my $rc = GetOptions("codespeed" => \$codespeed,
+ "reponame=s" => \$reponame,
+ "sort-by=s" => \$sortby,
+ "subsection=s" => \$subsection);
+usage() unless $rc;
+
while (scalar @ARGV) {
my $arg = $ARGV[0];
my $dir;
- if ($arg eq "--codespeed") {
- $codespeed = 1;
- shift @ARGV;
- next;
- }
- if ($arg =~ /--sort-by(?:=(.*))?/) {
- shift @ARGV;
- if (defined $1) {
- $sortby = $1;
- } else {
- $sortby = shift @ARGV;
- if (! defined $sortby) {
- die "'--sort-by' requires an argument";
- }
- }
- next;
- }
- if ($arg eq "--subsection") {
- shift @ARGV;
- $subsection = $ARGV[0];
- shift @ARGV;
- if (! $subsection) {
- die "empty subsection";
- }
- next;
- }
- if ($arg eq "--reponame") {
- shift @ARGV;
- $reponame = $ARGV[0];
- shift @ARGV;
- if (! $reponame) {
- die "empty reponame";
- }
- next;
- }
last if -f $arg or $arg eq "--";
if (! -d $arg) {
my $rev = Git::command_oneline(qw(rev-parse --verify), $arg);
my ($sortby) = @_;
if ($sortby ne "regression") {
- die "only 'regression' is supported as '--sort-by' argument";
+ print "Only 'regression' is supported as '--sort-by' argument\n";
+ usage();
}
my @evolutions;
GIT_PERF_DIRS_OR_REVS="$bisect_head"
export GIT_PERF_DIRS_OR_REVS
+# Don't use codespeed
+GIT_PERF_CODESPEED_OUTPUT=
+GIT_PERF_SEND_TO_CODESPEED=
+export GIT_PERF_CODESPEED_OUTPUT
+export GIT_PERF_SEND_TO_CODESPEED
+
./run "$script" >"$result_file" 2>&1 || die "Failed to run perf test '$script'"
rtime=$(sed -n "s/^$script_number\.$test_number:.*\([0-9]\+\.[0-9]\+\)(.*).*\$/\1/p" "$result_file")
test_submodule_relative_url "(null)" "user@host:path/to/repo" "../subrepo" "user@host:path/to/subrepo"
test_submodule_relative_url "(null)" "user@host:repo" "../subrepo" "user@host:subrepo"
+test_expect_success 'match .gitmodules' '
+ test-tool path-utils is_dotgitmodules \
+ .gitmodules \
+ \
+ .git${u200c}modules \
+ \
+ .Gitmodules \
+ .gitmoduleS \
+ \
+ ".gitmodules " \
+ ".gitmodules." \
+ ".gitmodules " \
+ ".gitmodules. " \
+ ".gitmodules ." \
+ ".gitmodules.." \
+ ".gitmodules " \
+ ".gitmodules. " \
+ ".gitmodules . " \
+ ".gitmodules ." \
+ \
+ ".Gitmodules " \
+ ".Gitmodules." \
+ ".Gitmodules " \
+ ".Gitmodules. " \
+ ".Gitmodules ." \
+ ".Gitmodules.." \
+ ".Gitmodules " \
+ ".Gitmodules. " \
+ ".Gitmodules . " \
+ ".Gitmodules ." \
+ \
+ GITMOD~1 \
+ gitmod~1 \
+ GITMOD~2 \
+ gitmod~3 \
+ GITMOD~4 \
+ \
+ "GITMOD~1 " \
+ "gitmod~2." \
+ "GITMOD~3 " \
+ "gitmod~4. " \
+ "GITMOD~1 ." \
+ "gitmod~2 " \
+ "GITMOD~3. " \
+ "gitmod~4 . " \
+ \
+ GI7EBA~1 \
+ gi7eba~9 \
+ \
+ GI7EB~10 \
+ GI7EB~11 \
+ GI7EB~99 \
+ GI7EB~10 \
+ GI7E~100 \
+ GI7E~101 \
+ GI7E~999 \
+ ~1000000 \
+ ~9999999 \
+ \
+ --not \
+ ".gitmodules x" \
+ ".gitmodules .x" \
+ \
+ " .gitmodules" \
+ \
+ ..gitmodules \
+ \
+ gitmodules \
+ \
+ .gitmodule \
+ \
+ ".gitmodules x " \
+ ".gitmodules .x" \
+ \
+ GI7EBA~ \
+ GI7EBA~0 \
+ GI7EBA~~1 \
+ GI7EBA~X \
+ Gx7EBA~1 \
+ GI7EBX~1 \
+ \
+ GI7EB~1 \
+ GI7EB~01 \
+ GI7EB~1X
+'
+
test_done
test OTHER = $(git cat-file blob "master@{2005-05-26 23:42}:F")
'
+# Test adding and deleting pseudorefs
+
+test_expect_success 'given old value for missing pseudoref, do not create' '
+ test_must_fail git update-ref PSEUDOREF $A $B 2>err &&
+ test_path_is_missing .git/PSEUDOREF &&
+ grep "could not read ref" err
+'
+
+test_expect_success 'create pseudoref' '
+ git update-ref PSEUDOREF $A &&
+ test $A = $(cat .git/PSEUDOREF)
+'
+
+test_expect_success 'overwrite pseudoref with no old value given' '
+ git update-ref PSEUDOREF $B &&
+ test $B = $(cat .git/PSEUDOREF)
+'
+
+test_expect_success 'overwrite pseudoref with correct old value' '
+ git update-ref PSEUDOREF $C $B &&
+ test $C = $(cat .git/PSEUDOREF)
+'
+
+test_expect_success 'do not overwrite pseudoref with wrong old value' '
+ test_must_fail git update-ref PSEUDOREF $D $E 2>err &&
+ test $C = $(cat .git/PSEUDOREF) &&
+ grep "unexpected object ID" err
+'
+
+test_expect_success 'delete pseudoref' '
+ git update-ref -d PSEUDOREF &&
+ test_path_is_missing .git/PSEUDOREF
+'
+
+test_expect_success 'do not delete pseudoref with wrong old value' '
+ git update-ref PSEUDOREF $A &&
+ test_must_fail git update-ref -d PSEUDOREF $B 2>err &&
+ test $A = $(cat .git/PSEUDOREF) &&
+ grep "unexpected object ID" err
+'
+
+test_expect_success 'delete pseudoref with correct old value' '
+ git update-ref -d PSEUDOREF $A &&
+ test_path_is_missing .git/PSEUDOREF
+'
+
+test_expect_success 'create pseudoref with old OID zero' '
+ git update-ref PSEUDOREF $A $Z &&
+ test $A = $(cat .git/PSEUDOREF)
+'
+
+test_expect_success 'do not overwrite pseudoref with old OID zero' '
+ test_when_finished git update-ref -d PSEUDOREF &&
+ test_must_fail git update-ref PSEUDOREF $B $Z 2>err &&
+ test $A = $(cat .git/PSEUDOREF) &&
+ grep "already exists" err
+'
+
+# Test --stdin
+
a=refs/heads/a
b=refs/heads/b
c=refs/heads/c
git -c core.disambiguate=committish rev-parse $sha1^{tree}
'
+test_expect_success C_LOCALE_OUTPUT 'ambiguous commits are printed by type first, then hash order' '
+ test_must_fail git rev-parse 0000 2>stderr &&
+ grep ^hint: stderr >hints &&
+ grep 0000 hints >objects &&
+ cat >expected <<-\EOF &&
+ tag
+ commit
+ tree
+ blob
+ EOF
+ awk "{print \$3}" <objects >objects.types &&
+ uniq <objects.types >objects.types.uniq &&
+ test_cmp expected objects.types.uniq &&
+ for type in tag commit tree blob
+ do
+ grep $type objects >$type.objects &&
+ sort $type.objects >$type.objects.sorted &&
+ test_cmp $type.objects.sorted $type.objects
+ done
+'
+
test_done
test_cmp_rev HEAD bat
'
-test_expect_success '"add" auto-vivify does not clobber existing branch' '
- test_commit c1 &&
- test_commit c2 &&
- git branch precious HEAD~1 &&
- test_must_fail git worktree add precious &&
- test_cmp_rev HEAD~1 precious &&
- test_path_is_missing precious
+test_expect_success '"add" checks out existing branch of dwimd name' '
+ git branch dwim HEAD~1 &&
+ git worktree add dwim &&
+ test_cmp_rev HEAD~1 dwim &&
+ (
+ cd dwim &&
+ test_cmp_rev HEAD dwim
+ )
+'
+
+test_expect_success '"add <path>" dwim fails with checked out branch' '
+ git checkout -b test-branch &&
+ test_must_fail git worktree add test-branch &&
+ test_path_is_missing test-branch
+'
+
+test_expect_success '"add --force" with existing dwimd name doesnt die' '
+ git checkout test-branch &&
+ git worktree add --force test-branch
'
test_expect_success '"add" no auto-vivify with --detach and <branch> omitted' '
check_threshold_0
'
+test_expect_success 'merge.renames disables rename detection' '
+ git read-tree --reset -u HEAD &&
+ git -c merge.renames=false merge-recursive $tail &&
+ check_no_renames
+'
+
+test_expect_success 'merge.renames defaults to diff.renames' '
+ git read-tree --reset -u HEAD &&
+ git -c diff.renames=false merge-recursive $tail &&
+ check_no_renames
+'
+
+test_expect_success 'merge.renames overrides diff.renames' '
+ git read-tree --reset -u HEAD &&
+ test_must_fail git -c diff.renames=false -c merge.renames=true merge-recursive $tail &&
+ $check_50
+'
+
test_done
test A = $(git cat-file commit HEAD^^ | sed -ne \$p)
'
-cat >expect <<EOF
-Successfully rebased and updated refs/heads/missing-commit.
-EOF
-
test_expect_success 'rebase -i respects rebase.missingCommitsCheck = ignore' '
test_config rebase.missingCommitsCheck ignore &&
rebase_setup_and_clean missing-commit &&
FAKE_LINES="1 2 3 4" \
git rebase -i --root 2>actual &&
test D = $(git cat-file commit HEAD | sed -ne \$p) &&
- test_i18ncmp expect actual
+ test_i18ngrep \
+ "Successfully rebased and updated refs/heads/missing-commit" \
+ actual
'
cat >expect <<EOF
Use 'git config rebase.missingCommitsCheck' to change the level of warnings.
The possible behaviours are: ignore, warn, error.
+Rebasing (1/4)
+Rebasing (2/4)
+Rebasing (3/4)
+Rebasing (4/4)
Successfully rebased and updated refs/heads/missing-commit.
EOF
+cr_to_nl () {
+ tr '\015' '\012'
+}
+
test_expect_success 'rebase -i respects rebase.missingCommitsCheck = warn' '
test_config rebase.missingCommitsCheck warn &&
rebase_setup_and_clean missing-commit &&
set_fake_editor &&
FAKE_LINES="1 2 3 4" \
- git rebase -i --root 2>actual &&
+ git rebase -i --root 2>actual.2 &&
+ cr_to_nl <actual.2 >actual &&
test_i18ncmp expect actual &&
test D = $(git cat-file commit HEAD | sed -ne \$p)
'
git rebase --continue
'
+test_expect_success '--skip after failed fixup cleans commit message' '
+ test_when_finished "test_might_fail git rebase --abort" &&
+ git checkout -b with-conflicting-fixup &&
+ test_commit wants-fixup &&
+ test_commit "fixup! wants-fixup" wants-fixup.t 1 wants-fixup-1 &&
+ test_commit "fixup! wants-fixup" wants-fixup.t 2 wants-fixup-2 &&
+ test_commit "fixup! wants-fixup" wants-fixup.t 3 wants-fixup-3 &&
+ test_must_fail env FAKE_LINES="1 fixup 2 squash 4" \
+ git rebase -i HEAD~4 &&
+
+ : now there is a conflict, and comments in the commit message &&
+ git show HEAD >out &&
+ grep "fixup! wants-fixup" out &&
+
+ : skip and continue &&
+ echo "cp \"\$1\" .git/copy.txt" | write_script copy-editor.sh &&
+ (test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) &&
+
+ : the user should not have had to edit the commit message &&
+ test_path_is_missing .git/copy.txt &&
+
+ : now the comments in the commit message should have been cleaned up &&
+ git show HEAD >out &&
+ ! grep "fixup! wants-fixup" out &&
+
+ : now, let us ensure that "squash" is handled correctly &&
+ git reset --hard wants-fixup-3 &&
+ test_must_fail env FAKE_LINES="1 squash 4 squash 2 squash 4" \
+ git rebase -i HEAD~4 &&
+
+ : the first squash failed, but there are two more in the chain &&
+ (test_set_editor "$PWD/copy-editor.sh" &&
+ test_must_fail git rebase --skip) &&
+
+ : not the final squash, no need to edit the commit message &&
+ test_path_is_missing .git/copy.txt &&
+
+ : The first squash was skipped, therefore: &&
+ git show HEAD >out &&
+ test_i18ngrep "# This is a combination of 2 commits" out &&
+
+ (test_set_editor "$PWD/copy-editor.sh" && git rebase --skip) &&
+ git show HEAD >out &&
+ test_i18ngrep ! "# This is a combination" out &&
+
+ : Final squash failed, but there was still a squash &&
+ test_i18ngrep "# This is a combination of 2 commits" .git/copy.txt
+'
+
test_expect_success 'setup rerere database' '
rm -fr .git/rebase-* &&
git reset --hard commit-new-file-F3-on-topic-branch &&
test_run_rebase success -m
test_run_rebase success -i
test_run_rebase failure -p
+test_run_rebase success --rebase-merges
# m
# /
test_cmp_rev c HEAD
"
}
-test_run_rebase failure ''
-test_run_rebase failure -m
-test_run_rebase failure -i
+test_run_rebase success ''
+test_run_rebase success -m
+test_run_rebase success -i
test_run_rebase failure -p
test_run_rebase () {
--- /dev/null
+#!/bin/sh
+#
+# Copyright (c) 2018 Johannes E. Schindelin
+#
+
+test_description='git rebase -i --rebase-merges
+
+This test runs git rebase "interactively", retaining the branch structure by
+recreating merge commits.
+
+Initial setup:
+
+ -- B -- (first)
+ / \
+ A - C - D - E - H (master)
+ \ /
+ F - G (second)
+'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-rebase.sh
+
+test_cmp_graph () {
+ cat >expect &&
+ git log --graph --boundary --format=%s "$@" >output &&
+ sed "s/ *$//" <output >output.trimmed &&
+ test_cmp expect output.trimmed
+}
+
+test_expect_success 'setup' '
+ write_script replace-editor.sh <<-\EOF &&
+ mv "$1" "$(git rev-parse --git-path ORIGINAL-TODO)"
+ cp script-from-scratch "$1"
+ EOF
+
+ test_commit A &&
+ git checkout -b first &&
+ test_commit B &&
+ git checkout master &&
+ test_commit C &&
+ test_commit D &&
+ git merge --no-commit B &&
+ test_tick &&
+ git commit -m E &&
+ git tag -m E E &&
+ git checkout -b second C &&
+ test_commit F &&
+ test_commit G &&
+ git checkout master &&
+ git merge --no-commit G &&
+ test_tick &&
+ git commit -m H &&
+ git tag -m H H
+'
+
+test_expect_success 'create completely different structure' '
+ cat >script-from-scratch <<-\EOF &&
+ label onto
+
+ # onebranch
+ pick G
+ pick D
+ label onebranch
+
+ # second
+ reset onto
+ pick B
+ label second
+
+ reset onto
+ merge -C H second
+ merge onebranch # Merge the topic branch '\''onebranch'\''
+ EOF
+ test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+ test_tick &&
+ git rebase -i -r A &&
+ test_cmp_graph <<-\EOF
+ * Merge the topic branch '\''onebranch'\''
+ |\
+ | * D
+ | * G
+ * | H
+ |\ \
+ | |/
+ |/|
+ | * B
+ |/
+ * A
+ EOF
+'
+
+test_expect_success 'generate correct todo list' '
+ cat >expect <<-\EOF &&
+ label onto
+
+ reset onto
+ pick d9df450 B
+ label E
+
+ reset onto
+ pick 5dee784 C
+ label branch-point
+ pick ca2c861 F
+ pick 088b00a G
+ label H
+
+ reset branch-point # C
+ pick 12bd07b D
+ merge -C 2051b56 E # E
+ merge -C 233d48a H # H
+
+ EOF
+
+ grep -v "^#" <.git/ORIGINAL-TODO >output &&
+ test_cmp expect output
+'
+
+test_expect_success '`reset` refuses to overwrite untracked files' '
+ git checkout -b refuse-to-reset &&
+ test_commit dont-overwrite-untracked &&
+ git checkout @{-1} &&
+ : >dont-overwrite-untracked.t &&
+ echo "reset refs/tags/dont-overwrite-untracked" >script-from-scratch &&
+ test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+ test_must_fail git rebase -r HEAD &&
+ git rebase --abort
+'
+
+test_expect_success 'failed `merge` writes patch (may be rescheduled, too)' '
+ test_when_finished "test_might_fail git rebase --abort" &&
+ git checkout -b conflicting-merge A &&
+
+ : fail because of conflicting untracked file &&
+ >G.t &&
+ echo "merge -C H G" >script-from-scratch &&
+ test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+ test_tick &&
+ test_must_fail git rebase -ir HEAD &&
+ grep "^merge -C .* G$" .git/rebase-merge/done &&
+ grep "^merge -C .* G$" .git/rebase-merge/git-rebase-todo &&
+ test_path_is_file .git/rebase-merge/patch &&
+
+ : fail because of merge conflict &&
+ rm G.t .git/rebase-merge/patch &&
+ git reset --hard &&
+ test_commit conflicting-G G.t not-G conflicting-G &&
+ test_must_fail git rebase --continue &&
+ ! grep "^merge -C .* G$" .git/rebase-merge/git-rebase-todo &&
+ test_path_is_file .git/rebase-merge/patch
+'
+
+test_expect_success 'with a branch tip that was cherry-picked already' '
+ git checkout -b already-upstream master &&
+ base="$(git rev-parse --verify HEAD)" &&
+
+ test_commit A1 &&
+ test_commit A2 &&
+ git reset --hard $base &&
+ test_commit B1 &&
+ test_tick &&
+ git merge -m "Merge branch A" A2 &&
+
+ git checkout -b upstream-with-a2 $base &&
+ test_tick &&
+ git cherry-pick A2 &&
+
+ git checkout already-upstream &&
+ test_tick &&
+ git rebase -i -r upstream-with-a2 &&
+ test_cmp_graph upstream-with-a2.. <<-\EOF
+ * Merge branch A
+ |\
+ | * A1
+ * | B1
+ |/
+ o A2
+ EOF
+'
+
+test_expect_success 'do not rebase cousins unless asked for' '
+ git checkout -b cousins master &&
+ before="$(git rev-parse --verify HEAD)" &&
+ test_tick &&
+ git rebase -r HEAD^ &&
+ test_cmp_rev HEAD $before &&
+ test_tick &&
+ git rebase --rebase-merges=rebase-cousins HEAD^ &&
+ test_cmp_graph HEAD^.. <<-\EOF
+ * Merge the topic branch '\''onebranch'\''
+ |\
+ | * D
+ | * G
+ |/
+ o H
+ EOF
+'
+
+test_expect_success 'refs/rewritten/* is worktree-local' '
+ git worktree add wt &&
+ cat >wt/script-from-scratch <<-\EOF &&
+ label xyz
+ exec GIT_DIR=../.git git rev-parse --verify refs/rewritten/xyz >a || :
+ exec git rev-parse --verify refs/rewritten/xyz >b
+ EOF
+
+ test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
+ git -C wt rebase -i HEAD &&
+ test_must_be_empty wt/a &&
+ test_cmp_rev HEAD "$(cat wt/b)"
+'
+
+test_expect_success 'post-rewrite hook and fixups work for merges' '
+ git checkout -b post-rewrite &&
+ test_commit same1 &&
+ git reset --hard HEAD^ &&
+ test_commit same2 &&
+ git merge -m "to fix up" same1 &&
+ echo same old same old >same2.t &&
+ test_tick &&
+ git commit --fixup HEAD same2.t &&
+ fixup="$(git rev-parse HEAD)" &&
+
+ mkdir -p .git/hooks &&
+ test_when_finished "rm .git/hooks/post-rewrite" &&
+ echo "cat >actual" | write_script .git/hooks/post-rewrite &&
+
+ test_tick &&
+ git rebase -i --autosquash -r HEAD^^^ &&
+ printf "%s %s\n%s %s\n%s %s\n%s %s\n" >expect $(git rev-parse \
+ $fixup^^2 HEAD^2 \
+ $fixup^^ HEAD^ \
+ $fixup^ HEAD \
+ $fixup HEAD) &&
+ test_cmp expect actual
+'
+
+test_expect_success 'refuse to merge ancestors of HEAD' '
+ echo "merge HEAD^" >script-from-scratch &&
+ test_config -C wt sequence.editor \""$PWD"/replace-editor.sh\" &&
+ before="$(git rev-parse HEAD)" &&
+ git rebase -i HEAD &&
+ test_cmp_rev HEAD $before
+'
+
+test_expect_success 'root commits' '
+ git checkout --orphan unrelated &&
+ (GIT_AUTHOR_NAME="Parsnip" GIT_AUTHOR_EMAIL="root@example.com" \
+ test_commit second-root) &&
+ test_commit third-root &&
+ cat >script-from-scratch <<-\EOF &&
+ pick third-root
+ label first-branch
+ reset [new root]
+ pick second-root
+ merge first-branch # Merge the 3rd root
+ EOF
+ test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+ test_tick &&
+ git rebase -i --force --root -r &&
+ test "Parsnip" = "$(git show -s --format=%an HEAD^)" &&
+ test $(git rev-parse second-root^0) != $(git rev-parse HEAD^) &&
+ test $(git rev-parse second-root:second-root.t) = \
+ $(git rev-parse HEAD^:second-root.t) &&
+ test_cmp_graph HEAD <<-\EOF &&
+ * Merge the 3rd root
+ |\
+ | * third-root
+ * second-root
+ EOF
+
+ : fast forward if possible &&
+ before="$(git rev-parse --verify HEAD)" &&
+ test_might_fail git config --unset sequence.editor &&
+ test_tick &&
+ git rebase -i --root -r &&
+ test_cmp_rev HEAD $before
+'
+
+test_expect_success 'a "merge" into a root commit is a fast-forward' '
+ head=$(git rev-parse HEAD) &&
+ cat >script-from-scratch <<-EOF &&
+ reset [new root]
+ merge $head
+ EOF
+ test_config sequence.editor \""$PWD"/replace-editor.sh\" &&
+ test_tick &&
+ git rebase -i -r HEAD^ &&
+ test_cmp_rev HEAD $head
+'
+
+test_expect_success 'A root commit can be a cousin, treat it that way' '
+ git checkout --orphan khnum &&
+ test_commit yama &&
+ git checkout -b asherah master &&
+ test_commit shamkat &&
+ git merge --allow-unrelated-histories khnum &&
+ test_tick &&
+ git rebase -f -r HEAD^ &&
+ ! test_cmp_rev HEAD^2 khnum &&
+ test_cmp_graph HEAD^.. <<-\EOF &&
+ * Merge branch '\''khnum'\'' into asherah
+ |\
+ | * yama
+ o shamkat
+ EOF
+ test_tick &&
+ git rebase --rebase-merges=rebase-cousins HEAD^ &&
+ test_cmp_graph HEAD^.. <<-\EOF
+ * Merge branch '\''khnum'\'' into asherah
+ |\
+ | * yama
+ |/
+ o shamkat
+ EOF
+'
+
+test_done
test_cmp expect actual
'
-test_expect_failure 'cherry-pick works with dirty renamed file' '
+test_expect_success 'cherry-pick works with dirty renamed file' '
test_commit to-rename &&
git checkout -b unrelated &&
test_commit unrelated &&
test_tick &&
git commit -m renamed &&
echo modified >renamed &&
- test_must_fail git cherry-pick refs/heads/unrelated >out &&
- test_i18ngrep "Refusing to lose dirty file at renamed" out &&
- test $(git rev-parse :0:renamed) = $(git rev-parse HEAD^:to-rename.t) &&
+ git cherry-pick refs/heads/unrelated >out &&
+ test $(git rev-parse :0:renamed) = $(git rev-parse HEAD~2:to-rename.t) &&
grep -q "^modified$" renamed
'
test_i18ngrep "renamed: .*path1 -> subdir/path1" out
'
+test_expect_success 'test diff.renames=true for git status' '
+ git -c diff.renames=true status >out &&
+ test_i18ngrep "renamed: .*path1 -> subdir/path1" out
+'
+
+test_expect_success 'test diff.renames=false for git status' '
+ git -c diff.renames=false status >out &&
+ test_i18ngrep ! "renamed: .*path1 -> subdir/path1" out &&
+ test_i18ngrep "new file: .*subdir/path1" out &&
+ test_i18ngrep "deleted: .*[^/]path1" out
+'
+
test_expect_success 'favour same basenames even with minor differences' '
git show HEAD:path1 | sed "s/15/16/" > subdir/path1 &&
git status >out &&
test_write_lines 1 2 >expect &&
test_cmp expect actual
'
+test_expect_success 'format-patch --attach cover-letter only is non-multipart' '
+ test_when_finished "rm -fr patches" &&
+ git format-patch -o patches --cover-letter --attach=mimemime --base=HEAD~ -1 &&
+ ! egrep "^--+mimemime" patches/0000*.patch &&
+ egrep "^--+mimemime$" patches/0001*.patch >output &&
+ test_line_count = 2 output &&
+ egrep "^--+mimemime--$" patches/0001*.patch >output &&
+ test_line_count = 1 output
+'
test_expect_success 'format-patch --pretty=mboxrd' '
sp=" " &&
grep -F "no threads support, ignoring pack.threads" err
'
+test_expect_success 'pack-objects in too-many-packs mode' '
+ GIT_TEST_FULL_IN_PACK_ARRAY=1 git repack -ad &&
+ git fsck
+'
+
#
# WARNING!
#
'
test_expect_success JGIT 'we can read jgit bitmaps' '
- git clone . compat-jgit &&
+ git clone --bare . compat-jgit.git &&
(
- cd compat-jgit &&
+ cd compat-jgit.git &&
rm -f .git/objects/pack/*.bitmap &&
jgit gc &&
git rev-list --test-bitmap HEAD
'
test_expect_success JGIT 'jgit can read our bitmaps' '
- git clone . compat-us &&
+ git clone --bare . compat-us.git &&
(
- cd compat-us &&
+ cd compat-us.git &&
git repack -adb &&
# jgit gc will barf if it does not like our bitmaps
jgit gc
grep refs/tags/magic actual
'
-git fetch origin
test_expect_success 'ls-remote --symref' '
+ git fetch origin &&
cat >expect <<-EOF &&
ref: refs/heads/master HEAD
$(git rev-parse HEAD) HEAD
}
check_push_result () {
+ test $# -ge 3 ||
+ error "bug in the test script: check_push_result requires at least 3 parameters"
+
repo_name="$1"
shift
test_expect_success 'push with dry-run' '
mk_test testrepo heads/master &&
- (
- cd testrepo &&
- old_commit=$(git show-ref -s --verify refs/heads/master)
- ) &&
+ old_commit=$(git -C testrepo show-ref -s --verify refs/heads/master) &&
git push --dry-run testrepo : &&
check_push_result testrepo $old_commit heads/master
'
chmod +x testrepo/.git/hooks/pre-receive &&
(
cd child &&
- git pull .. master
+ git pull .. master &&
test_must_fail git push &&
test $(git rev-parse master) != \
$(git rev-parse remotes/origin/master)
agent=git/$(git version | cut -d" " -f3)
ls-refs
fetch=shallow
+ server-option
0000
EOF
test_cmp actual expect
'
+test_expect_success 'sending server-options' '
+ test-pkt-line pack >in <<-EOF &&
+ command=ls-refs
+ server-option=hello
+ server-option=world
+ 0001
+ ref-prefix HEAD
+ 0000
+ EOF
+
+ cat >expect <<-EOF &&
+ $(git rev-parse HEAD) HEAD
+ 0000
+ EOF
+
+ git serve --stateless-rpc <in >out &&
+ test-pkt-line unpack <out >actual &&
+ test_cmp actual expect
+'
+
+test_expect_success 'unexpected lines are not allowed in fetch request' '
+ git init server &&
+
+ test-pkt-line pack >in <<-EOF &&
+ command=fetch
+ 0001
+ this-is-not-a-command
+ 0000
+ EOF
+
+ test_must_fail git -C server serve --stateless-rpc <in >/dev/null 2>err &&
+ grep "unexpected line: .this-is-not-a-command." err
+'
+
test_done
test_cmp actual expect
'
+test_expect_success 'server-options are sent when using ls-remote' '
+ test_when_finished "rm -f log" &&
+
+ GIT_TRACE_PACKET="$(pwd)/log" git -c protocol.version=2 \
+ ls-remote -o hello -o world "file://$(pwd)/file_parent" master >actual &&
+
+ cat >expect <<-EOF &&
+ $(git -C file_parent rev-parse refs/heads/master)$(printf "\t")refs/heads/master
+ EOF
+
+ test_cmp actual expect &&
+ grep "server-option=hello" log &&
+ grep "server-option=world" log
+'
+
+
test_expect_success 'clone with file:// using protocol v2' '
test_when_finished "rm -f log" &&
! grep "refs/tags/three" log
'
+test_expect_success 'server-options are sent when fetching' '
+ test_when_finished "rm -f log" &&
+
+ test_commit -C file_parent four &&
+
+ GIT_TRACE_PACKET="$(pwd)/log" git -C file_child -c protocol.version=2 \
+ fetch -o hello -o world origin master &&
+
+ git -C file_child log -1 --format=%s origin/master >actual &&
+ git -C file_parent log -1 --format=%s >expect &&
+ test_cmp expect actual &&
+
+ grep "server-option=hello" log &&
+ grep "server-option=world" log
+'
+
+test_expect_success 'upload-pack respects config using protocol v2' '
+ git init server &&
+ write_script server/.git/hook <<-\EOF &&
+ touch hookout
+ "$@"
+ EOF
+ test_commit -C server one &&
+
+ test_config_global uploadpack.packobjectshook ./hook &&
+ test_path_is_missing server/.git/hookout &&
+ git -c protocol.version=2 clone "file://$(pwd)/server" client &&
+ test_path_is_file server/.git/hookout
+'
+
+test_expect_success 'setup filter tests' '
+ rm -rf server client &&
+ git init server &&
+
+ # 1 commit to create a file, and 1 commit to modify it
+ test_commit -C server message1 a.txt &&
+ test_commit -C server message2 a.txt &&
+ git -C server config protocol.version 2 &&
+ git -C server config uploadpack.allowfilter 1 &&
+ git -C server config uploadpack.allowanysha1inwant 1 &&
+ git -C server config protocol.version 2
+'
+
+test_expect_success 'partial clone' '
+ GIT_TRACE_PACKET="$(pwd)/trace" git -c protocol.version=2 \
+ clone --filter=blob:none "file://$(pwd)/server" client &&
+ grep "version 2" trace &&
+
+ # Ensure that the old version of the file is missing
+ git -C client rev-list master --quiet --objects --missing=print \
+ >observed.oids &&
+ grep "$(git -C server rev-parse message1:a.txt)" observed.oids &&
+
+ # Ensure that client passes fsck
+ git -C client fsck
+'
+
+test_expect_success 'dynamically fetch missing object' '
+ rm "$(pwd)/trace" &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client -c protocol.version=2 \
+ cat-file -p $(git -C server rev-parse message1:a.txt) &&
+ grep "version 2" trace
+'
+
+test_expect_success 'partial fetch' '
+ rm -rf client "$(pwd)/trace" &&
+ git init client &&
+ SERVER="file://$(pwd)/server" &&
+ test_config -C client extensions.partialClone "$SERVER" &&
+
+ GIT_TRACE_PACKET="$(pwd)/trace" git -C client -c protocol.version=2 \
+ fetch --filter=blob:none "$SERVER" master:refs/heads/other &&
+ grep "version 2" trace &&
+
+ # Ensure that the old version of the file is missing
+ git -C client rev-list other --quiet --objects --missing=print \
+ >observed.oids &&
+ grep "$(git -C server rev-parse message1:a.txt)" observed.oids &&
+
+ # Ensure that client passes fsck
+ git -C client fsck
+'
+
+test_expect_success 'do not advertise filter if not configured to do so' '
+ SERVER="file://$(pwd)/server" &&
+
+ rm "$(pwd)/trace" &&
+ git -C server config uploadpack.allowfilter 1 &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -c protocol.version=2 \
+ ls-remote "$SERVER" &&
+ grep "fetch=.*filter" trace &&
+
+ rm "$(pwd)/trace" &&
+ git -C server config uploadpack.allowfilter 0 &&
+ GIT_TRACE_PACKET="$(pwd)/trace" git -c protocol.version=2 \
+ ls-remote "$SERVER" &&
+ grep "fetch=" trace >fetch_capabilities &&
+ ! grep filter fetch_capabilities
+'
+
+test_expect_success 'partial clone warns if filter is not advertised' '
+ rm -rf client &&
+ git -C server config uploadpack.allowfilter 0 &&
+ git -c protocol.version=2 \
+ clone --filter=blob:none "file://$(pwd)/server" client 2>err &&
+ test_i18ngrep "filtering not recognized by server, ignoring" err
+'
+
+test_expect_success 'even with handcrafted request, filter does not work if not advertised' '
+ git -C server config uploadpack.allowfilter 0 &&
+
+ # Custom request that tries to filter even though it is not advertised.
+ test-pkt-line pack >in <<-EOF &&
+ command=fetch
+ 0001
+ want $(git -C server rev-parse master)
+ filter blob:none
+ 0000
+ EOF
+
+ test_must_fail git -C server serve --stateless-rpc <in >/dev/null 2>err &&
+ grep "unexpected line: .filter blob:none." err &&
+
+ # Exercise to ensure that if advertised, filter works
+ git -C server config uploadpack.allowfilter 1 &&
+ git -C server serve --stateless-rpc <in >/dev/null
+'
+
+test_expect_success 'default refspec is used to filter ref when fetchcing' '
+ test_when_finished "rm -f log" &&
+
+ GIT_TRACE_PACKET="$(pwd)/log" git -C file_child -c protocol.version=2 \
+ fetch origin &&
+
+ git -C file_child log -1 --format=%s three >actual &&
+ git -C file_parent log -1 --format=%s three >expect &&
+ test_cmp expect actual &&
+
+ grep "ref-prefix refs/heads/" log &&
+ grep "ref-prefix refs/tags/" log
+'
+
# Test protocol v2 with 'http://' transport
#
. "$TEST_DIRECTORY"/lib-httpd.sh
"
done
+
+test_expect_success 'show advice that grafts are deprecated' '
+ git show HEAD 2>err &&
+ test_i18ngrep "git replace" err &&
+ test_config advice.graftFileDeprecated false &&
+ git show HEAD 2>err &&
+ test_i18ngrep ! "git replace" err
+'
+
test_done
git reset --hard HEAD^ &&
git checkout change &&
GIT_MERGE_VERBOSITY=3 git merge change+rename >out &&
- test_i18ngrep "^Skipped B" out
+ test_i18ngrep ! "^Skipped B" out
'
test_expect_success 'setup for rename + d/f conflicts' '
--- /dev/null
+#!/bin/sh
+
+test_description="recursive merge with directory renames"
+# includes checking of many corner cases, with a similar methodology to:
+# t6042: corner cases with renames but not criss-cross merges
+# t6036: corner cases with both renames and criss-cross merges
+#
+# The setup for all of them, pictorially, is:
+#
+# A
+# o
+# / \
+# O o ?
+# \ /
+# o
+# B
+#
+# To help make it easier to follow the flow of tests, they have been
+# divided into sections and each test will start with a quick explanation
+# of what commits O, A, and B contain.
+#
+# Notation:
+# z/{b,c} means files z/b and z/c both exist
+# x/d_1 means file x/d exists with content d1. (Purpose of the
+# underscore notation is to differentiate different
+# files that might be renamed into each other's paths.)
+
+. ./test-lib.sh
+
+
+###########################################################################
+# SECTION 1: Basic cases we should be able to handle
+###########################################################################
+
+# Testcase 1a, Basic directory rename.
+# Commit O: z/{b,c}
+# Commit A: y/{b,c}
+# Commit B: z/{b,c,d,e/f}
+# Expected: y/{b,c,d,e/f}
+
+test_expect_success '1a-setup: Simple directory rename detection' '
+ test_create_repo 1a &&
+ (
+ cd 1a &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo d >z/d &&
+ mkdir z/e &&
+ echo f >z/e/f &&
+ git add z/d z/e/f &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '1a-check: Simple directory rename detection' '
+ (
+ cd 1a &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 4 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:y/d HEAD:y/e/f &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/d B:z/e/f &&
+ test_cmp expect actual &&
+
+ git hash-object y/d >actual &&
+ git rev-parse B:z/d >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse HEAD:z/d &&
+ test_must_fail git rev-parse HEAD:z/e/f &&
+ test_path_is_missing z/d &&
+ test_path_is_missing z/e/f
+ )
+'
+
+# Testcase 1b, Merge a directory with another
+# Commit O: z/{b,c}, y/d
+# Commit A: z/{b,c,e}, y/d
+# Commit B: y/{b,c,d}
+# Expected: y/{b,c,d,e}
+
+test_expect_success '1b-setup: Merge a directory with another' '
+ test_create_repo 1b &&
+ (
+ cd 1b &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir y &&
+ echo d >y/d &&
+ git add z y &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ echo e >z/e &&
+ git add z/e &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv z/b y &&
+ git mv z/c y &&
+ rmdir z &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '1b-check: Merge a directory with another' '
+ (
+ cd 1b &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 4 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:y/d HEAD:y/e &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:y/d A:z/e &&
+ test_cmp expect actual &&
+ test_must_fail git rev-parse HEAD:z/e
+ )
+'
+
+# Testcase 1c, Transitive renaming
+# (Related to testcases 3a and 6d -- when should a transitive rename apply?)
+# (Related to testcases 9c and 9d -- can transitivity repeat?)
+# (Related to testcase 12b -- joint-transitivity?)
+# Commit O: z/{b,c}, x/d
+# Commit A: y/{b,c}, x/d
+# Commit B: z/{b,c,d}
+# Expected: y/{b,c,d} (because x/d -> z/d -> y/d)
+
+test_expect_success '1c-setup: Transitive renaming' '
+ test_create_repo 1c &&
+ (
+ cd 1c &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir x &&
+ echo d >x/d &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/d z/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '1c-check: Transitive renaming' '
+ (
+ cd 1c &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:y/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:x/d &&
+ test_cmp expect actual &&
+ test_must_fail git rev-parse HEAD:x/d &&
+ test_must_fail git rev-parse HEAD:z/d &&
+ test_path_is_missing z/d
+ )
+'
+
+# Testcase 1d, Directory renames (merging two directories into one new one)
+# cause a rename/rename(2to1) conflict
+# (Related to testcases 1c and 7b)
+# Commit O. z/{b,c}, y/{d,e}
+# Commit A. x/{b,c}, y/{d,e,m,wham_1}
+# Commit B. z/{b,c,n,wham_2}, x/{d,e}
+# Expected: x/{b,c,d,e,m,n}, CONFLICT:(y/wham_1 & z/wham_2 -> x/wham)
+# Note: y/m & z/n should definitely move into x. By the same token, both
+# y/wham_1 & z/wham_2 should too...giving us a conflict.
+
+test_expect_success '1d-setup: Directory renames cause a rename/rename(2to1) conflict' '
+ test_create_repo 1d &&
+ (
+ cd 1d &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir y &&
+ echo d >y/d &&
+ echo e >y/e &&
+ git add z y &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z x &&
+ echo m >y/m &&
+ echo wham1 >y/wham &&
+ git add y &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv y x &&
+ echo n >z/n &&
+ echo wham2 >z/wham &&
+ git add z &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '1d-check: Directory renames cause a rename/rename(2to1) conflict' '
+ (
+ cd 1d &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (rename/rename)" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 8 out &&
+ git ls-files -u >out &&
+ test_line_count = 2 out &&
+ git ls-files -o >out &&
+ test_line_count = 3 out &&
+
+ git rev-parse >actual \
+ :0:x/b :0:x/c :0:x/d :0:x/e :0:x/m :0:x/n &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:y/d O:y/e A:y/m B:z/n &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse :0:x/wham &&
+ git rev-parse >actual \
+ :2:x/wham :3:x/wham &&
+ git rev-parse >expect \
+ A:y/wham B:z/wham &&
+ test_cmp expect actual &&
+
+ test_path_is_missing x/wham &&
+ test_path_is_file x/wham~HEAD &&
+ test_path_is_file x/wham~B^0 &&
+
+ git hash-object >actual \
+ x/wham~HEAD x/wham~B^0 &&
+ git rev-parse >expect \
+ A:y/wham B:z/wham &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 1e, Renamed directory, with all filenames being renamed too
+# (Related to testcases 9f & 9g)
+# Commit O: z/{oldb,oldc}
+# Commit A: y/{newb,newc}
+# Commit B: z/{oldb,oldc,d}
+# Expected: y/{newb,newc,d}
+
+test_expect_success '1e-setup: Renamed directory, with all files being renamed too' '
+ test_create_repo 1e &&
+ (
+ cd 1e &&
+
+ mkdir z &&
+ echo b >z/oldb &&
+ echo c >z/oldc &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ mkdir y &&
+ git mv z/oldb y/newb &&
+ git mv z/oldc y/newc &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo d >z/d &&
+ git add z/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '1e-check: Renamed directory, with all files being renamed too' '
+ (
+ cd 1e &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+
+ git rev-parse >actual \
+ HEAD:y/newb HEAD:y/newc HEAD:y/d &&
+ git rev-parse >expect \
+ O:z/oldb O:z/oldc B:z/d &&
+ test_cmp expect actual &&
+ test_must_fail git rev-parse HEAD:z/d
+ )
+'
+
+# Testcase 1f, Split a directory into two other directories
+# (Related to testcases 3a, all of section 2, and all of section 4)
+# Commit O: z/{b,c,d,e,f}
+# Commit A: z/{b,c,d,e,f,g}
+# Commit B: y/{b,c}, x/{d,e,f}
+# Expected: y/{b,c}, x/{d,e,f,g}
+
+test_expect_success '1f-setup: Split a directory into two other directories' '
+ test_create_repo 1f &&
+ (
+ cd 1f &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ echo d >z/d &&
+ echo e >z/e &&
+ echo f >z/f &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ echo g >z/g &&
+ git add z/g &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ mkdir y &&
+ mkdir x &&
+ git mv z/b y/ &&
+ git mv z/c y/ &&
+ git mv z/d x/ &&
+ git mv z/e x/ &&
+ git mv z/f x/ &&
+ rmdir z &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '1f-check: Split a directory into two other directories' '
+ (
+ cd 1f &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 6 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:x/d HEAD:x/e HEAD:x/f HEAD:x/g &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:z/d O:z/e O:z/f A:z/g &&
+ test_cmp expect actual &&
+ test_path_is_missing z/g &&
+ test_must_fail git rev-parse HEAD:z/g
+ )
+'
+
+###########################################################################
+# Rules suggested by testcases in section 1:
+#
+# We should still detect the directory rename even if it wasn't just
+# the directory renamed, but the files within it. (see 1b)
+#
+# If renames split a directory into two or more others, the directory
+# with the most renames, "wins" (see 1c). However, see the testcases
+# in section 2, plus testcases 3a and 4a.
+###########################################################################
+
+
+###########################################################################
+# SECTION 2: Split into multiple directories, with equal number of paths
+#
+# Explore the splitting-a-directory rules a bit; what happens in the
+# edge cases?
+#
+# Note that there is a closely related case of a directory not being
+# split on either side of history, but being renamed differently on
+# each side. See testcase 8e for that.
+###########################################################################
+
+# Testcase 2a, Directory split into two on one side, with equal numbers of paths
+# Commit O: z/{b,c}
+# Commit A: y/b, w/c
+# Commit B: z/{b,c,d}
+# Expected: y/b, w/c, z/d, with warning about z/ -> (y/ vs. w/) conflict
+test_expect_success '2a-setup: Directory split into two on one side, with equal numbers of paths' '
+ test_create_repo 2a &&
+ (
+ cd 2a &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ mkdir y &&
+ mkdir w &&
+ git mv z/b y/ &&
+ git mv z/c w/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo d >z/d &&
+ git add z/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '2a-check: Directory split into two on one side, with equal numbers of paths' '
+ (
+ cd 2a &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT.*directory rename split" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:w/c :0:z/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/d &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 2b, Directory split into two on one side, with equal numbers of paths
+# Commit O: z/{b,c}
+# Commit A: y/b, w/c
+# Commit B: z/{b,c}, x/d
+# Expected: y/b, w/c, x/d; No warning about z/ -> (y/ vs. w/) conflict
+test_expect_success '2b-setup: Directory split into two on one side, with equal numbers of paths' '
+ test_create_repo 2b &&
+ (
+ cd 2b &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ mkdir y &&
+ mkdir w &&
+ git mv z/b y/ &&
+ git mv z/c w/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ mkdir x &&
+ echo d >x/d &&
+ git add x/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '2b-check: Directory split into two on one side, with equal numbers of paths' '
+ (
+ cd 2b &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 >out &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:w/c :0:x/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:x/d &&
+ test_cmp expect actual &&
+ test_i18ngrep ! "CONFLICT.*directory rename split" out
+ )
+'
+
+###########################################################################
+# Rules suggested by section 2:
+#
+# None; the rule was already covered in section 1. These testcases are
+# here just to make sure the conflict resolution and necessary warning
+# messages are handled correctly.
+###########################################################################
+
+
+###########################################################################
+# SECTION 3: Path in question is the source path for some rename already
+#
+# Combining cases from Section 1 and trying to handle them could lead to
+# directory renaming detection being over-applied. So, this section
+# provides some good testcases to check that the implementation doesn't go
+# too far.
+###########################################################################
+
+# Testcase 3a, Avoid implicit rename if involved as source on other side
+# (Related to testcases 1c, 1f, and 9h)
+# Commit O: z/{b,c,d}
+# Commit A: z/{b,c,d} (no change)
+# Commit B: y/{b,c}, x/d
+# Expected: y/{b,c}, x/d
+test_expect_success '3a-setup: Avoid implicit rename if involved as source on other side' '
+ test_create_repo 3a &&
+ (
+ cd 3a &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ echo d >z/d &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_tick &&
+ git commit --allow-empty -m "A" &&
+
+ git checkout B &&
+ mkdir y &&
+ mkdir x &&
+ git mv z/b y/ &&
+ git mv z/c y/ &&
+ git mv z/d x/ &&
+ rmdir z &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '3a-check: Avoid implicit rename if involved as source on other side' '
+ (
+ cd 3a &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:x/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:z/d &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 3b, Avoid implicit rename if involved as source on other side
+# (Related to testcases 5c and 7c, also kind of 1e and 1f)
+# Commit O: z/{b,c,d}
+# Commit A: y/{b,c}, x/d
+# Commit B: z/{b,c}, w/d
+# Expected: y/{b,c}, CONFLICT:(z/d -> x/d vs. w/d)
+# NOTE: We're particularly checking that since z/d is already involved as
+# a source in a file rename on the same side of history, that we don't
+# get it involved in directory rename detection. If it were, we might
+# end up with CONFLICT:(z/d -> y/d vs. x/d vs. w/d), i.e. a
+# rename/rename/rename(1to3) conflict, which is just weird.
+test_expect_success '3b-setup: Avoid implicit rename if involved as source on current side' '
+ test_create_repo 3b &&
+ (
+ cd 3b &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ echo d >z/d &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ mkdir y &&
+ mkdir x &&
+ git mv z/b y/ &&
+ git mv z/c y/ &&
+ git mv z/d x/ &&
+ rmdir z &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ mkdir w &&
+ git mv z/d w/ &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '3b-check: Avoid implicit rename if involved as source on current side' '
+ (
+ cd 3b &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep CONFLICT.*rename/rename.*z/d.*x/d.*w/d out &&
+ test_i18ngrep ! CONFLICT.*rename/rename.*y/d out &&
+
+ git ls-files -s >out &&
+ test_line_count = 5 out &&
+ git ls-files -u >out &&
+ test_line_count = 3 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:y/c :1:z/d :2:x/d :3:w/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:z/d O:z/d O:z/d &&
+ test_cmp expect actual &&
+
+ test_path_is_missing z/d &&
+ git hash-object >actual \
+ x/d w/d &&
+ git rev-parse >expect \
+ O:z/d O:z/d &&
+ test_cmp expect actual
+ )
+'
+
+###########################################################################
+# Rules suggested by section 3:
+#
+# Avoid directory-rename-detection for a path, if that path is the source
+# of a rename on either side of a merge.
+###########################################################################
+
+
+###########################################################################
+# SECTION 4: Partially renamed directory; still exists on both sides of merge
+#
+# What if we were to attempt to do directory rename detection when someone
+# "mostly" moved a directory but still left some files around, or,
+# equivalently, fully renamed a directory in one commmit and then recreated
+# that directory in a later commit adding some new files and then tried to
+# merge?
+#
+# It's hard to divine user intent in these cases, because you can make an
+# argument that, depending on the intermediate history of the side being
+# merged, that some users will want files in that directory to
+# automatically be detected and renamed, while users with a different
+# intermediate history wouldn't want that rename to happen.
+#
+# I think that it is best to simply not have directory rename detection
+# apply to such cases. My reasoning for this is four-fold: (1) it's
+# easiest for users in general to figure out what happened if we don't
+# apply directory rename detection in any such case, (2) it's an easy rule
+# to explain ["We don't do directory rename detection if the directory
+# still exists on both sides of the merge"], (3) we can get some hairy
+# edge/corner cases that would be really confusing and possibly not even
+# representable in the index if we were to even try, and [related to 3] (4)
+# attempting to resolve this issue of divining user intent by examining
+# intermediate history goes against the spirit of three-way merges and is a
+# path towards crazy corner cases that are far more complex than what we're
+# already dealing with.
+#
+# Note that the wording of the rule ("We don't do directory rename
+# detection if the directory still exists on both sides of the merge.")
+# also excludes "renaming" of a directory into a subdirectory of itself
+# (e.g. /some/dir/* -> /some/dir/subdir/*). It may be possible to carve
+# out an exception for "renaming"-beneath-itself cases without opening
+# weird edge/corner cases for other partial directory renames, but for now
+# we are keeping the rule simple.
+#
+# This section contains a test for a partially-renamed-directory case.
+###########################################################################
+
+# Testcase 4a, Directory split, with original directory still present
+# (Related to testcase 1f)
+# Commit O: z/{b,c,d,e}
+# Commit A: y/{b,c,d}, z/e
+# Commit B: z/{b,c,d,e,f}
+# Expected: y/{b,c,d}, z/{e,f}
+# NOTE: Even though most files from z moved to y, we don't want f to follow.
+
+test_expect_success '4a-setup: Directory split, with original directory still present' '
+ test_create_repo 4a &&
+ (
+ cd 4a &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ echo d >z/d &&
+ echo e >z/e &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ mkdir y &&
+ git mv z/b y/ &&
+ git mv z/c y/ &&
+ git mv z/d y/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo f >z/f &&
+ git add z/f &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '4a-check: Directory split, with original directory still present' '
+ (
+ cd 4a &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 5 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:y/d HEAD:z/e HEAD:z/f &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:z/d O:z/e B:z/f &&
+ test_cmp expect actual
+ )
+'
+
+###########################################################################
+# Rules suggested by section 4:
+#
+# Directory-rename-detection should be turned off for any directories (as
+# a source for renames) that exist on both sides of the merge. (The "as
+# a source for renames" clarification is due to cases like 1c where
+# the target directory exists on both sides and we do want the rename
+# detection.) But, sadly, see testcase 8b.
+###########################################################################
+
+
+###########################################################################
+# SECTION 5: Files/directories in the way of subset of to-be-renamed paths
+#
+# Implicitly renaming files due to a detected directory rename could run
+# into problems if there are files or directories in the way of the paths
+# we want to rename. Explore such cases in this section.
+###########################################################################
+
+# Testcase 5a, Merge directories, other side adds files to original and target
+# Commit O: z/{b,c}, y/d
+# Commit A: z/{b,c,e_1,f}, y/{d,e_2}
+# Commit B: y/{b,c,d}
+# Expected: z/e_1, y/{b,c,d,e_2,f} + CONFLICT warning
+# NOTE: While directory rename detection is active here causing z/f to
+# become y/f, we did not apply this for z/e_1 because that would
+# give us an add/add conflict for y/e_1 vs y/e_2. This problem with
+# this add/add, is that both versions of y/e are from the same side
+# of history, giving us no way to represent this conflict in the
+# index.
+
+test_expect_success '5a-setup: Merge directories, other side adds files to original and target' '
+ test_create_repo 5a &&
+ (
+ cd 5a &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir y &&
+ echo d >y/d &&
+ git add z y &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ echo e1 >z/e &&
+ echo f >z/f &&
+ echo e2 >y/e &&
+ git add z/e z/f y/e &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv z/b y/ &&
+ git mv z/c y/ &&
+ rmdir z &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '5a-check: Merge directories, other side adds files to original and target' '
+ (
+ cd 5a &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT.*implicit dir rename" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 6 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:y/c :0:y/d :0:y/e :0:z/e :0:y/f &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:y/d A:y/e A:z/e A:z/f &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 5b, Rename/delete in order to get add/add/add conflict
+# (Related to testcase 8d; these may appear slightly inconsistent to users;
+# Also related to testcases 7d and 7e)
+# Commit O: z/{b,c,d_1}
+# Commit A: y/{b,c,d_2}
+# Commit B: z/{b,c,d_1,e}, y/d_3
+# Expected: y/{b,c,e}, CONFLICT(add/add: y/d_2 vs. y/d_3)
+# NOTE: If z/d_1 in commit B were to be involved in dir rename detection, as
+# we normaly would since z/ is being renamed to y/, then this would be
+# a rename/delete (z/d_1 -> y/d_1 vs. deleted) AND an add/add/add
+# conflict of y/d_1 vs. y/d_2 vs. y/d_3. Add/add/add is not
+# representable in the index, so the existence of y/d_3 needs to
+# cause us to bail on directory rename detection for that path, falling
+# back to git behavior without the directory rename detection.
+
+test_expect_success '5b-setup: Rename/delete in order to get add/add/add conflict' '
+ test_create_repo 5b &&
+ (
+ cd 5b &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ echo d1 >z/d &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git rm z/d &&
+ git mv z y &&
+ echo d2 >y/d &&
+ git add y/d &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ mkdir y &&
+ echo d3 >y/d &&
+ echo e >z/e &&
+ git add y/d z/e &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '5b-check: Rename/delete in order to get add/add/add conflict' '
+ (
+ cd 5b &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (add/add).* y/d" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 5 out &&
+ git ls-files -u >out &&
+ test_line_count = 2 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:y/c :0:y/e :2:y/d :3:y/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/e A:y/d B:y/d &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse :1:y/d &&
+ test_path_is_file y/d
+ )
+'
+
+# Testcase 5c, Transitive rename would cause rename/rename/rename/add/add/add
+# (Directory rename detection would result in transitive rename vs.
+# rename/rename(1to2) and turn it into a rename/rename(1to3). Further,
+# rename paths conflict with separate adds on the other side)
+# (Related to testcases 3b and 7c)
+# Commit O: z/{b,c}, x/d_1
+# Commit A: y/{b,c,d_2}, w/d_1
+# Commit B: z/{b,c,d_1,e}, w/d_3, y/d_4
+# Expected: A mess, but only a rename/rename(1to2)/add/add mess. Use the
+# presence of y/d_4 in B to avoid doing transitive rename of
+# x/d_1 -> z/d_1 -> y/d_1, so that the only paths we have at
+# y/d are y/d_2 and y/d_4. We still do the move from z/e to y/e,
+# though, because it doesn't have anything in the way.
+
+test_expect_success '5c-setup: Transitive rename would cause rename/rename/rename/add/add/add' '
+ test_create_repo 5c &&
+ (
+ cd 5c &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir x &&
+ echo d1 >x/d &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ echo d2 >y/d &&
+ git add y/d &&
+ git mv x w &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/d z/ &&
+ mkdir w &&
+ mkdir y &&
+ echo d3 >w/d &&
+ echo d4 >y/d &&
+ echo e >z/e &&
+ git add w/ y/ z/e &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '5c-check: Transitive rename would cause rename/rename/rename/add/add/add' '
+ (
+ cd 5c &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (rename/rename).*x/d.*w/d.*z/d" out &&
+ test_i18ngrep "CONFLICT (add/add).* y/d" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 9 out &&
+ git ls-files -u >out &&
+ test_line_count = 6 out &&
+ git ls-files -o >out &&
+ test_line_count = 3 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:y/c :0:y/e &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/e &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse :1:y/d &&
+ git rev-parse >actual \
+ :2:w/d :3:w/d :1:x/d :2:y/d :3:y/d :3:z/d &&
+ git rev-parse >expect \
+ O:x/d B:w/d O:x/d A:y/d B:y/d O:x/d &&
+ test_cmp expect actual &&
+
+ git hash-object >actual \
+ w/d~HEAD w/d~B^0 z/d &&
+ git rev-parse >expect \
+ O:x/d B:w/d O:x/d &&
+ test_cmp expect actual &&
+ test_path_is_missing x/d &&
+ test_path_is_file y/d &&
+ grep -q "<<<<" y/d # conflict markers should be present
+ )
+'
+
+# Testcase 5d, Directory/file/file conflict due to directory rename
+# Commit O: z/{b,c}
+# Commit A: y/{b,c,d_1}
+# Commit B: z/{b,c,d_2,f}, y/d/e
+# Expected: y/{b,c,d/e,f}, z/d_2, CONFLICT(file/directory), y/d_1~HEAD
+# Note: The fact that y/d/ exists in B makes us bail on directory rename
+# detection for z/d_2, but that doesn't prevent us from applying the
+# directory rename detection for z/f -> y/f.
+
+test_expect_success '5d-setup: Directory/file/file conflict due to directory rename' '
+ test_create_repo 5d &&
+ (
+ cd 5d &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ echo d1 >y/d &&
+ git add y/d &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ mkdir -p y/d &&
+ echo e >y/d/e &&
+ echo d2 >z/d &&
+ echo f >z/f &&
+ git add y/d/e z/d z/f &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '5d-check: Directory/file/file conflict due to directory rename' '
+ (
+ cd 5d &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (file/directory).*y/d" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 6 out &&
+ git ls-files -u >out &&
+ test_line_count = 1 out &&
+ git ls-files -o >out &&
+ test_line_count = 2 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:y/c :0:z/d :0:y/f :2:y/d :0:y/d/e &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/d B:z/f A:y/d B:y/d/e &&
+ test_cmp expect actual &&
+
+ git hash-object y/d~HEAD >actual &&
+ git rev-parse A:y/d >expect &&
+ test_cmp expect actual
+ )
+'
+
+###########################################################################
+# Rules suggested by section 5:
+#
+# If a subset of to-be-renamed files have a file or directory in the way,
+# "turn off" the directory rename for those specific sub-paths, falling
+# back to old handling. But, sadly, see testcases 8a and 8b.
+###########################################################################
+
+
+###########################################################################
+# SECTION 6: Same side of the merge was the one that did the rename
+#
+# It may sound obvious that you only want to apply implicit directory
+# renames to directories if the _other_ side of history did the renaming.
+# If you did make an implementation that didn't explicitly enforce this
+# rule, the majority of cases that would fall under this section would
+# also be solved by following the rules from the above sections. But
+# there are still a few that stick out, so this section covers them just
+# to make sure we also get them right.
+###########################################################################
+
+# Testcase 6a, Tricky rename/delete
+# Commit O: z/{b,c,d}
+# Commit A: z/b
+# Commit B: y/{b,c}, z/d
+# Expected: y/b, CONFLICT(rename/delete, z/c -> y/c vs. NULL)
+# Note: We're just checking here that the rename of z/b and z/c to put
+# them under y/ doesn't accidentally catch z/d and make it look like
+# it is also involved in a rename/delete conflict.
+
+test_expect_success '6a-setup: Tricky rename/delete' '
+ test_create_repo 6a &&
+ (
+ cd 6a &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ echo d >z/d &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git rm z/c &&
+ git rm z/d &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ mkdir y &&
+ git mv z/b y/ &&
+ git mv z/c y/ &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '6a-check: Tricky rename/delete' '
+ (
+ cd 6a &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (rename/delete).*z/c.*y/c" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 2 out &&
+ git ls-files -u >out &&
+ test_line_count = 1 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ :0:y/b :3:y/c &&
+ git rev-parse >expect \
+ O:z/b O:z/c &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 6b, Same rename done on both sides
+# (Related to testcases 6c and 8e)
+# Commit O: z/{b,c}
+# Commit A: y/{b,c}
+# Commit B: y/{b,c}, z/d
+# Expected: y/{b,c}, z/d
+# Note: If we did directory rename detection here, we'd move z/d into y/,
+# but B did that rename and still decided to put the file into z/,
+# so we probably shouldn't apply directory rename detection for it.
+
+test_expect_success '6b-setup: Same rename done on both sides' '
+ test_create_repo 6b &&
+ (
+ cd 6b &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv z y &&
+ mkdir z &&
+ echo d >z/d &&
+ git add z/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '6b-check: Same rename done on both sides' '
+ (
+ cd 6b &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:z/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/d &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 6c, Rename only done on same side
+# (Related to testcases 6b and 8e)
+# Commit O: z/{b,c}
+# Commit A: z/{b,c} (no change)
+# Commit B: y/{b,c}, z/d
+# Expected: y/{b,c}, z/d
+# NOTE: Seems obvious, but just checking that the implementation doesn't
+# "accidentally detect a rename" and give us y/{b,c,d}.
+
+test_expect_success '6c-setup: Rename only done on same side' '
+ test_create_repo 6c &&
+ (
+ cd 6c &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_tick &&
+ git commit --allow-empty -m "A" &&
+
+ git checkout B &&
+ git mv z y &&
+ mkdir z &&
+ echo d >z/d &&
+ git add z/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '6c-check: Rename only done on same side' '
+ (
+ cd 6c &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:z/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/d &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 6d, We don't always want transitive renaming
+# (Related to testcase 1c)
+# Commit O: z/{b,c}, x/d
+# Commit A: z/{b,c}, x/d (no change)
+# Commit B: y/{b,c}, z/d
+# Expected: y/{b,c}, z/d
+# NOTE: Again, this seems obvious but just checking that the implementation
+# doesn't "accidentally detect a rename" and give us y/{b,c,d}.
+
+test_expect_success '6d-setup: We do not always want transitive renaming' '
+ test_create_repo 6d &&
+ (
+ cd 6d &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir x &&
+ echo d >x/d &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_tick &&
+ git commit --allow-empty -m "A" &&
+
+ git checkout B &&
+ git mv z y &&
+ git mv x z &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '6d-check: We do not always want transitive renaming' '
+ (
+ cd 6d &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:z/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:x/d &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 6e, Add/add from one-side
+# Commit O: z/{b,c}
+# Commit A: z/{b,c} (no change)
+# Commit B: y/{b,c,d_1}, z/d_2
+# Expected: y/{b,c,d_1}, z/d_2
+# NOTE: Again, this seems obvious but just checking that the implementation
+# doesn't "accidentally detect a rename" and give us y/{b,c} +
+# add/add conflict on y/d_1 vs y/d_2.
+
+test_expect_success '6e-setup: Add/add from one side' '
+ test_create_repo 6e &&
+ (
+ cd 6e &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_tick &&
+ git commit --allow-empty -m "A" &&
+
+ git checkout B &&
+ git mv z y &&
+ echo d1 > y/d &&
+ mkdir z &&
+ echo d2 > z/d &&
+ git add y/d z/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '6e-check: Add/add from one side' '
+ (
+ cd 6e &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 4 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:y/d HEAD:z/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:y/d B:z/d &&
+ test_cmp expect actual
+ )
+'
+
+###########################################################################
+# Rules suggested by section 6:
+#
+# Only apply implicit directory renames to directories if the other
+# side of history is the one doing the renaming.
+###########################################################################
+
+
+###########################################################################
+# SECTION 7: More involved Edge/Corner cases
+#
+# The ruleset we have generated in the above sections seems to provide
+# well-defined merges. But can we find edge/corner cases that either (a)
+# are harder for users to understand, or (b) have a resolution that is
+# non-intuitive or suboptimal?
+#
+# The testcases in this section dive into cases that I've tried to craft in
+# a way to find some that might be surprising to users or difficult for
+# them to understand (the next section will look at non-intuitive or
+# suboptimal merge results). Some of the testcases are similar to ones
+# from past sections, but have been simplified to try to highlight error
+# messages using a "modified" path (due to the directory rename). Are
+# users okay with these?
+#
+# In my opinion, testcases that are difficult to understand from this
+# section is due to difficulty in the testcase rather than the directory
+# renaming (similar to how t6042 and t6036 have difficult resolutions due
+# to the problem setup itself being complex). And I don't think the
+# error messages are a problem.
+#
+# On the other hand, the testcases in section 8 worry me slightly more...
+###########################################################################
+
+# Testcase 7a, rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file
+# Commit O: z/{b,c}
+# Commit A: y/{b,c}
+# Commit B: w/b, x/c, z/d
+# Expected: y/d, CONFLICT(rename/rename for both z/b and z/c)
+# NOTE: There's a rename of z/ here, y/ has more renames, so z/d -> y/d.
+
+test_expect_success '7a-setup: rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file' '
+ test_create_repo 7a &&
+ (
+ cd 7a &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ mkdir w &&
+ mkdir x &&
+ git mv z/b w/ &&
+ git mv z/c x/ &&
+ echo d > z/d &&
+ git add z/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '7a-check: rename-dir vs. rename-dir (NOT split evenly) PLUS add-other-file' '
+ (
+ cd 7a &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (rename/rename).*z/b.*y/b.*w/b" out &&
+ test_i18ngrep "CONFLICT (rename/rename).*z/c.*y/c.*x/c" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 7 out &&
+ git ls-files -u >out &&
+ test_line_count = 6 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ :1:z/b :2:y/b :3:w/b :1:z/c :2:y/c :3:x/c :0:y/d &&
+ git rev-parse >expect \
+ O:z/b O:z/b O:z/b O:z/c O:z/c O:z/c B:z/d &&
+ test_cmp expect actual &&
+
+ git hash-object >actual \
+ y/b w/b y/c x/c &&
+ git rev-parse >expect \
+ O:z/b O:z/b O:z/c O:z/c &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 7b, rename/rename(2to1), but only due to transitive rename
+# (Related to testcase 1d)
+# Commit O: z/{b,c}, x/d_1, w/d_2
+# Commit A: y/{b,c,d_2}, x/d_1
+# Commit B: z/{b,c,d_1}, w/d_2
+# Expected: y/{b,c}, CONFLICT(rename/rename(2to1): x/d_1, w/d_2 -> y_d)
+
+test_expect_success '7b-setup: rename/rename(2to1), but only due to transitive rename' '
+ test_create_repo 7b &&
+ (
+ cd 7b &&
+
+ mkdir z &&
+ mkdir x &&
+ mkdir w &&
+ echo b >z/b &&
+ echo c >z/c &&
+ echo d1 > x/d &&
+ echo d2 > w/d &&
+ git add z x w &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ git mv w/d y/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/d z/ &&
+ rmdir x &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '7b-check: rename/rename(2to1), but only due to transitive rename' '
+ (
+ cd 7b &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (rename/rename)" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 4 out &&
+ git ls-files -u >out &&
+ test_line_count = 2 out &&
+ git ls-files -o >out &&
+ test_line_count = 3 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:y/c :2:y/d :3:y/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:w/d O:x/d &&
+ test_cmp expect actual &&
+
+ test_path_is_missing y/d &&
+ test_path_is_file y/d~HEAD &&
+ test_path_is_file y/d~B^0 &&
+
+ git hash-object >actual \
+ y/d~HEAD y/d~B^0 &&
+ git rev-parse >expect \
+ O:w/d O:x/d &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 7c, rename/rename(1to...2or3); transitive rename may add complexity
+# (Related to testcases 3b and 5c)
+# Commit O: z/{b,c}, x/d
+# Commit A: y/{b,c}, w/d
+# Commit B: z/{b,c,d}
+# Expected: y/{b,c}, CONFLICT(x/d -> w/d vs. y/d)
+# NOTE: z/ was renamed to y/ so we do want to report
+# neither CONFLICT(x/d -> w/d vs. z/d)
+# nor CONFLiCT x/d -> w/d vs. y/d vs. z/d)
+
+test_expect_success '7c-setup: rename/rename(1to...2or3); transitive rename may add complexity' '
+ test_create_repo 7c &&
+ (
+ cd 7c &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir x &&
+ echo d >x/d &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ git mv x w &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/d z/ &&
+ rmdir x &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '7c-check: rename/rename(1to...2or3); transitive rename may add complexity' '
+ (
+ cd 7c &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (rename/rename).*x/d.*w/d.*y/d" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 5 out &&
+ git ls-files -u >out &&
+ test_line_count = 3 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:y/c :1:x/d :2:w/d :3:y/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:x/d O:x/d O:x/d &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 7d, transitive rename involved in rename/delete; how is it reported?
+# (Related somewhat to testcases 5b and 8d)
+# Commit O: z/{b,c}, x/d
+# Commit A: y/{b,c}
+# Commit B: z/{b,c,d}
+# Expected: y/{b,c}, CONFLICT(delete x/d vs rename to y/d)
+# NOTE: z->y so NOT CONFLICT(delete x/d vs rename to z/d)
+
+test_expect_success '7d-setup: transitive rename involved in rename/delete; how is it reported?' '
+ test_create_repo 7d &&
+ (
+ cd 7d &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir x &&
+ echo d >x/d &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ git rm -rf x &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/d z/ &&
+ rmdir x &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '7d-check: transitive rename involved in rename/delete; how is it reported?' '
+ (
+ cd 7d &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (rename/delete).*x/d.*y/d" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+ git ls-files -u >out &&
+ test_line_count = 1 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:y/c :3:y/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:x/d &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 7e, transitive rename in rename/delete AND dirs in the way
+# (Very similar to 'both rename source and destination involved in D/F conflict' from t6022-merge-rename.sh)
+# (Also related to testcases 9c and 9d)
+# Commit O: z/{b,c}, x/d_1
+# Commit A: y/{b,c,d/g}, x/d/f
+# Commit B: z/{b,c,d_1}
+# Expected: rename/delete(x/d_1->y/d_1 vs. None) + D/F conflict on y/d
+# y/{b,c,d/g}, y/d_1~B^0, x/d/f
+
+# NOTE: The main path of interest here is d_1 and where it ends up, but
+# this is actually a case that has two potential directory renames
+# involved and D/F conflict(s), so it makes sense to walk through
+# each step.
+#
+# Commit A renames z/ -> y/. Thus everything that B adds to z/
+# should be instead moved to y/. This gives us the D/F conflict on
+# y/d because x/d_1 -> z/d_1 -> y/d_1 conflicts with y/d/g.
+#
+# Further, commit B renames x/ -> z/, thus everything A adds to x/
+# should instead be moved to z/...BUT we removed z/ and renamed it
+# to y/, so maybe everything should move not from x/ to z/, but
+# from x/ to z/ to y/. Doing so might make sense from the logic so
+# far, but note that commit A had both an x/ and a y/; it did the
+# renaming of z/ to y/ and created x/d/f and it clearly made these
+# things separate, so it doesn't make much sense to push these
+# together. Doing so is what I'd call a doubly transitive rename;
+# see testcases 9c and 9d for further discussion of this issue and
+# how it's resolved.
+
+test_expect_success '7e-setup: transitive rename in rename/delete AND dirs in the way' '
+ test_create_repo 7e &&
+ (
+ cd 7e &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir x &&
+ echo d1 >x/d &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ git rm x/d &&
+ mkdir -p x/d &&
+ mkdir -p y/d &&
+ echo f >x/d/f &&
+ echo g >y/d/g &&
+ git add x/d/f y/d/g &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/d z/ &&
+ rmdir x &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '7e-check: transitive rename in rename/delete AND dirs in the way' '
+ (
+ cd 7e &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (rename/delete).*x/d.*y/d" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 5 out &&
+ git ls-files -u >out &&
+ test_line_count = 1 out &&
+ git ls-files -o >out &&
+ test_line_count = 2 out &&
+
+ git rev-parse >actual \
+ :0:x/d/f :0:y/d/g :0:y/b :0:y/c :3:y/d &&
+ git rev-parse >expect \
+ A:x/d/f A:y/d/g O:z/b O:z/c O:x/d &&
+ test_cmp expect actual &&
+
+ git hash-object y/d~B^0 >actual &&
+ git rev-parse O:x/d >expect &&
+ test_cmp expect actual
+ )
+'
+
+###########################################################################
+# SECTION 8: Suboptimal merges
+#
+# As alluded to in the last section, the ruleset we have built up for
+# detecting directory renames unfortunately has some special cases where it
+# results in slightly suboptimal or non-intuitive behavior. This section
+# explores these cases.
+#
+# To be fair, we already had non-intuitive or suboptimal behavior for most
+# of these cases in git before introducing implicit directory rename
+# detection, but it'd be nice if there was a modified ruleset out there
+# that handled these cases a bit better.
+###########################################################################
+
+# Testcase 8a, Dual-directory rename, one into the others' way
+# Commit O. x/{a,b}, y/{c,d}
+# Commit A. x/{a,b,e}, y/{c,d,f}
+# Commit B. y/{a,b}, z/{c,d}
+#
+# Possible Resolutions:
+# w/o dir-rename detection: y/{a,b,f}, z/{c,d}, x/e
+# Currently expected: y/{a,b,e,f}, z/{c,d}
+# Optimal: y/{a,b,e}, z/{c,d,f}
+#
+# Note: Both x and y got renamed and it'd be nice to detect both, and we do
+# better with directory rename detection than git did without, but the
+# simple rule from section 5 prevents me from handling this as optimally as
+# we potentially could.
+
+test_expect_success '8a-setup: Dual-directory rename, one into the others way' '
+ test_create_repo 8a &&
+ (
+ cd 8a &&
+
+ mkdir x &&
+ mkdir y &&
+ echo a >x/a &&
+ echo b >x/b &&
+ echo c >y/c &&
+ echo d >y/d &&
+ git add x y &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ echo e >x/e &&
+ echo f >y/f &&
+ git add x/e y/f &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv y z &&
+ git mv x y &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '8a-check: Dual-directory rename, one into the others way' '
+ (
+ cd 8a &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 6 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ HEAD:y/a HEAD:y/b HEAD:y/e HEAD:y/f HEAD:z/c HEAD:z/d &&
+ git rev-parse >expect \
+ O:x/a O:x/b A:x/e A:y/f O:y/c O:y/d &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 8b, Dual-directory rename, one into the others' way, with conflicting filenames
+# Commit O. x/{a_1,b_1}, y/{a_2,b_2}
+# Commit A. x/{a_1,b_1,e_1}, y/{a_2,b_2,e_2}
+# Commit B. y/{a_1,b_1}, z/{a_2,b_2}
+#
+# w/o dir-rename detection: y/{a_1,b_1,e_2}, z/{a_2,b_2}, x/e_1
+# Currently expected: <same>
+# Scary: y/{a_1,b_1}, z/{a_2,b_2}, CONFLICT(add/add, e_1 vs. e_2)
+# Optimal: y/{a_1,b_1,e_1}, z/{a_2,b_2,e_2}
+#
+# Note: Very similar to 8a, except instead of 'e' and 'f' in directories x and
+# y, both are named 'e'. Without directory rename detection, neither file
+# moves directories. Implement directory rename detection suboptimally, and
+# you get an add/add conflict, but both files were added in commit A, so this
+# is an add/add conflict where one side of history added both files --
+# something we can't represent in the index. Obviously, we'd prefer the last
+# resolution, but our previous rules are too coarse to allow it. Using both
+# the rules from section 4 and section 5 save us from the Scary resolution,
+# making us fall back to pre-directory-rename-detection behavior for both
+# e_1 and e_2.
+
+test_expect_success '8b-setup: Dual-directory rename, one into the others way, with conflicting filenames' '
+ test_create_repo 8b &&
+ (
+ cd 8b &&
+
+ mkdir x &&
+ mkdir y &&
+ echo a1 >x/a &&
+ echo b1 >x/b &&
+ echo a2 >y/a &&
+ echo b2 >y/b &&
+ git add x y &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ echo e1 >x/e &&
+ echo e2 >y/e &&
+ git add x/e y/e &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv y z &&
+ git mv x y &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '8b-check: Dual-directory rename, one into the others way, with conflicting filenames' '
+ (
+ cd 8b &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 6 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ HEAD:y/a HEAD:y/b HEAD:z/a HEAD:z/b HEAD:x/e HEAD:y/e &&
+ git rev-parse >expect \
+ O:x/a O:x/b O:y/a O:y/b A:x/e A:y/e &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 8c, modify/delete or rename+modify/delete?
+# (Related to testcases 5b, 8d, and 9h)
+# Commit O: z/{b,c,d}
+# Commit A: y/{b,c}
+# Commit B: z/{b,c,d_modified,e}
+# Expected: y/{b,c,e}, CONFLICT(modify/delete: on z/d)
+#
+# Note: It could easily be argued that the correct resolution here is
+# y/{b,c,e}, CONFLICT(rename/delete: z/d -> y/d vs deleted)
+# and that the modifed version of d should be present in y/ after
+# the merge, just marked as conflicted. Indeed, I previously did
+# argue that. But applying directory renames to the side of
+# history where a file is merely modified results in spurious
+# rename/rename(1to2) conflicts -- see testcase 9h. See also
+# notes in 8d.
+
+test_expect_success '8c-setup: modify/delete or rename+modify/delete?' '
+ test_create_repo 8c &&
+ (
+ cd 8c &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ test_seq 1 10 >z/d &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git rm z/d &&
+ git mv z y &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo 11 >z/d &&
+ test_chmod +x z/d &&
+ echo e >z/e &&
+ git add z/d z/e &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '8c-check: modify/delete or rename+modify/delete' '
+ (
+ cd 8c &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ test_i18ngrep "CONFLICT (modify/delete).* z/d" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 5 out &&
+ git ls-files -u >out &&
+ test_line_count = 2 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ :0:y/b :0:y/c :0:y/e :1:z/d :3:z/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/e O:z/d B:z/d &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse :2:z/d &&
+ git ls-files -s z/d | grep ^100755 &&
+ test_path_is_file z/d &&
+ test_path_is_missing y/d
+ )
+'
+
+# Testcase 8d, rename/delete...or not?
+# (Related to testcase 5b; these may appear slightly inconsistent to users;
+# Also related to testcases 7d and 7e)
+# Commit O: z/{b,c,d}
+# Commit A: y/{b,c}
+# Commit B: z/{b,c,d,e}
+# Expected: y/{b,c,e}
+#
+# Note: It would also be somewhat reasonable to resolve this as
+# y/{b,c,e}, CONFLICT(rename/delete: x/d -> y/d or deleted)
+#
+# In this case, I'm leaning towards: commit A was the one that deleted z/d
+# and it did the rename of z to y, so the two "conflicts" (rename vs.
+# delete) are both coming from commit A, which is illogical. Conflicts
+# during merging are supposed to be about opposite sides doing things
+# differently.
+
+test_expect_success '8d-setup: rename/delete...or not?' '
+ test_create_repo 8d &&
+ (
+ cd 8d &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ test_seq 1 10 >z/d &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git rm z/d &&
+ git mv z y &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo e >z/e &&
+ git add z/e &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '8d-check: rename/delete...or not?' '
+ (
+ cd 8d &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:y/e &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/e &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 8e, Both sides rename, one side adds to original directory
+# Commit O: z/{b,c}
+# Commit A: y/{b,c}
+# Commit B: w/{b,c}, z/d
+#
+# Possible Resolutions:
+# w/o dir-rename detection: z/d, CONFLICT(z/b -> y/b vs. w/b),
+# CONFLICT(z/c -> y/c vs. w/c)
+# Currently expected: y/d, CONFLICT(z/b -> y/b vs. w/b),
+# CONFLICT(z/c -> y/c vs. w/c)
+# Optimal: ??
+#
+# Notes: In commit A, directory z got renamed to y. In commit B, directory z
+# did NOT get renamed; the directory is still present; instead it is
+# considered to have just renamed a subset of paths in directory z
+# elsewhere. Therefore, the directory rename done in commit A to z/
+# applies to z/d and maps it to y/d.
+#
+# It's possible that users would get confused about this, but what
+# should we do instead? Silently leaving at z/d seems just as bad or
+# maybe even worse. Perhaps we could print a big warning about z/d
+# and how we're moving to y/d in this case, but when I started thinking
+# about the ramifications of doing that, I didn't know how to rule out
+# that opening other weird edge and corner cases so I just punted.
+
+test_expect_success '8e-setup: Both sides rename, one side adds to original directory' '
+ test_create_repo 8e &&
+ (
+ cd 8e &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv z w &&
+ mkdir z &&
+ echo d >z/d &&
+ git add z/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '8e-check: Both sides rename, one side adds to original directory' '
+ (
+ cd 8e &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep CONFLICT.*rename/rename.*z/c.*y/c.*w/c out &&
+ test_i18ngrep CONFLICT.*rename/rename.*z/b.*y/b.*w/b out &&
+
+ git ls-files -s >out &&
+ test_line_count = 7 out &&
+ git ls-files -u >out &&
+ test_line_count = 6 out &&
+ git ls-files -o >out &&
+ test_line_count = 2 out &&
+
+ git rev-parse >actual \
+ :1:z/b :2:y/b :3:w/b :1:z/c :2:y/c :3:w/c :0:y/d &&
+ git rev-parse >expect \
+ O:z/b O:z/b O:z/b O:z/c O:z/c O:z/c B:z/d &&
+ test_cmp expect actual &&
+
+ git hash-object >actual \
+ y/b w/b y/c w/c &&
+ git rev-parse >expect \
+ O:z/b O:z/b O:z/c O:z/c &&
+ test_cmp expect actual &&
+
+ test_path_is_missing z/b &&
+ test_path_is_missing z/c
+ )
+'
+
+###########################################################################
+# SECTION 9: Other testcases
+#
+# This section consists of miscellaneous testcases I thought of during
+# the implementation which round out the testing.
+###########################################################################
+
+# Testcase 9a, Inner renamed directory within outer renamed directory
+# (Related to testcase 1f)
+# Commit O: z/{b,c,d/{e,f,g}}
+# Commit A: y/{b,c}, x/w/{e,f,g}
+# Commit B: z/{b,c,d/{e,f,g,h},i}
+# Expected: y/{b,c,i}, x/w/{e,f,g,h}
+# NOTE: The only reason this one is interesting is because when a directory
+# is split into multiple other directories, we determine by the weight
+# of which one had the most paths going to it. A naive implementation
+# of that could take the new file in commit B at z/i to x/w/i or x/i.
+
+test_expect_success '9a-setup: Inner renamed directory within outer renamed directory' '
+ test_create_repo 9a &&
+ (
+ cd 9a &&
+
+ mkdir -p z/d &&
+ echo b >z/b &&
+ echo c >z/c &&
+ echo e >z/d/e &&
+ echo f >z/d/f &&
+ echo g >z/d/g &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ mkdir x &&
+ git mv z/d x/w &&
+ git mv z y &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo h >z/d/h &&
+ echo i >z/i &&
+ git add z &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '9a-check: Inner renamed directory within outer renamed directory' '
+ (
+ cd 9a &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 7 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:y/i &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/i &&
+ test_cmp expect actual &&
+
+ git rev-parse >actual \
+ HEAD:x/w/e HEAD:x/w/f HEAD:x/w/g HEAD:x/w/h &&
+ git rev-parse >expect \
+ O:z/d/e O:z/d/f O:z/d/g B:z/d/h &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 9b, Transitive rename with content merge
+# (Related to testcase 1c)
+# Commit O: z/{b,c}, x/d_1
+# Commit A: y/{b,c}, x/d_2
+# Commit B: z/{b,c,d_3}
+# Expected: y/{b,c,d_merged}
+
+test_expect_success '9b-setup: Transitive rename with content merge' '
+ test_create_repo 9b &&
+ (
+ cd 9b &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir x &&
+ test_seq 1 10 >x/d &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ test_seq 1 11 >x/d &&
+ git add x/d &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ test_seq 0 10 >x/d &&
+ git mv x/d z/d &&
+ git add z/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '9b-check: Transitive rename with content merge' '
+ (
+ cd 9b &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+
+ test_seq 0 11 >expected &&
+ test_cmp expected y/d &&
+ git add expected &&
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:y/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c :0:expected &&
+ test_cmp expect actual &&
+ test_must_fail git rev-parse HEAD:x/d &&
+ test_must_fail git rev-parse HEAD:z/d &&
+ test_path_is_missing z/d &&
+
+ test $(git rev-parse HEAD:y/d) != $(git rev-parse O:x/d) &&
+ test $(git rev-parse HEAD:y/d) != $(git rev-parse A:x/d) &&
+ test $(git rev-parse HEAD:y/d) != $(git rev-parse B:z/d)
+ )
+'
+
+# Testcase 9c, Doubly transitive rename?
+# (Related to testcase 1c, 7e, and 9d)
+# Commit O: z/{b,c}, x/{d,e}, w/f
+# Commit A: y/{b,c}, x/{d,e,f,g}
+# Commit B: z/{b,c,d,e}, w/f
+# Expected: y/{b,c,d,e}, x/{f,g}
+#
+# NOTE: x/f and x/g may be slightly confusing here. The rename from w/f to
+# x/f is clear. Let's look beyond that. Here's the logic:
+# Commit B renamed x/ -> z/
+# Commit A renamed z/ -> y/
+# So, we could possibly further rename x/f to z/f to y/f, a doubly
+# transient rename. However, where does it end? We can chain these
+# indefinitely (see testcase 9d). What if there is a D/F conflict
+# at z/f/ or y/f/? Or just another file conflict at one of those
+# paths? In the case of an N-long chain of transient renamings,
+# where do we "abort" the rename at? Can the user make sense of
+# the resulting conflict and resolve it?
+#
+# To avoid this confusion I use the simple rule that if the other side
+# of history did a directory rename to a path that your side renamed
+# away, then ignore that particular rename from the other side of
+# history for any implicit directory renames.
+
+test_expect_success '9c-setup: Doubly transitive rename?' '
+ test_create_repo 9c &&
+ (
+ cd 9c &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ mkdir x &&
+ echo d >x/d &&
+ echo e >x/e &&
+ mkdir w &&
+ echo f >w/f &&
+ git add z x w &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z y &&
+ git mv w/f x/ &&
+ echo g >x/g &&
+ git add x/g &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/d z/d &&
+ git mv x/e z/e &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '9c-check: Doubly transitive rename?' '
+ (
+ cd 9c &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 >out &&
+ test_i18ngrep "WARNING: Avoiding applying x -> z rename to x/f" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 6 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:y/d HEAD:y/e HEAD:x/f HEAD:x/g &&
+ git rev-parse >expect \
+ O:z/b O:z/c O:x/d O:x/e O:w/f A:x/g &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 9d, N-fold transitive rename?
+# (Related to testcase 9c...and 1c and 7e)
+# Commit O: z/a, y/b, x/c, w/d, v/e, u/f
+# Commit A: y/{a,b}, w/{c,d}, u/{e,f}
+# Commit B: z/{a,t}, x/{b,c}, v/{d,e}, u/f
+# Expected: <see NOTE first>
+#
+# NOTE: z/ -> y/ (in commit A)
+# y/ -> x/ (in commit B)
+# x/ -> w/ (in commit A)
+# w/ -> v/ (in commit B)
+# v/ -> u/ (in commit A)
+# So, if we add a file to z, say z/t, where should it end up? In u?
+# What if there's another file or directory named 't' in one of the
+# intervening directories and/or in u itself? Also, shouldn't the
+# same logic that places 't' in u/ also move ALL other files to u/?
+# What if there are file or directory conflicts in any of them? If
+# we attempted to do N-way (N-fold? N-ary? N-uple?) transitive renames
+# like this, would the user have any hope of understanding any
+# conflicts or how their working tree ended up? I think not, so I'm
+# ruling out N-ary transitive renames for N>1.
+#
+# Therefore our expected result is:
+# z/t, y/a, x/b, w/c, u/d, u/e, u/f
+# The reason that v/d DOES get transitively renamed to u/d is that u/ isn't
+# renamed somewhere. A slightly sub-optimal result, but it uses fairly
+# simple rules that are consistent with what we need for all the other
+# testcases and simplifies things for the user.
+
+test_expect_success '9d-setup: N-way transitive rename?' '
+ test_create_repo 9d &&
+ (
+ cd 9d &&
+
+ mkdir z y x w v u &&
+ echo a >z/a &&
+ echo b >y/b &&
+ echo c >x/c &&
+ echo d >w/d &&
+ echo e >v/e &&
+ echo f >u/f &&
+ git add z y x w v u &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z/a y/ &&
+ git mv x/c w/ &&
+ git mv v/e u/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo t >z/t &&
+ git mv y/b x/ &&
+ git mv w/d v/ &&
+ git add z/t &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '9d-check: N-way transitive rename?' '
+ (
+ cd 9d &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 >out &&
+ test_i18ngrep "WARNING: Avoiding applying z -> y rename to z/t" out &&
+ test_i18ngrep "WARNING: Avoiding applying y -> x rename to y/a" out &&
+ test_i18ngrep "WARNING: Avoiding applying x -> w rename to x/b" out &&
+ test_i18ngrep "WARNING: Avoiding applying w -> v rename to w/c" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 7 out &&
+ git ls-files -o >out &&
+ test_line_count = 1 out &&
+
+ git rev-parse >actual \
+ HEAD:z/t \
+ HEAD:y/a HEAD:x/b HEAD:w/c \
+ HEAD:u/d HEAD:u/e HEAD:u/f &&
+ git rev-parse >expect \
+ B:z/t \
+ O:z/a O:y/b O:x/c \
+ O:w/d O:v/e A:u/f &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 9e, N-to-1 whammo
+# (Related to testcase 9c...and 1c and 7e)
+# Commit O: dir1/{a,b}, dir2/{d,e}, dir3/{g,h}, dirN/{j,k}
+# Commit A: dir1/{a,b,c,yo}, dir2/{d,e,f,yo}, dir3/{g,h,i,yo}, dirN/{j,k,l,yo}
+# Commit B: combined/{a,b,d,e,g,h,j,k}
+# Expected: combined/{a,b,c,d,e,f,g,h,i,j,k,l}, CONFLICT(Nto1) warnings,
+# dir1/yo, dir2/yo, dir3/yo, dirN/yo
+
+test_expect_success '9e-setup: N-to-1 whammo' '
+ test_create_repo 9e &&
+ (
+ cd 9e &&
+
+ mkdir dir1 dir2 dir3 dirN &&
+ echo a >dir1/a &&
+ echo b >dir1/b &&
+ echo d >dir2/d &&
+ echo e >dir2/e &&
+ echo g >dir3/g &&
+ echo h >dir3/h &&
+ echo j >dirN/j &&
+ echo k >dirN/k &&
+ git add dir* &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ echo c >dir1/c &&
+ echo yo >dir1/yo &&
+ echo f >dir2/f &&
+ echo yo >dir2/yo &&
+ echo i >dir3/i &&
+ echo yo >dir3/yo &&
+ echo l >dirN/l &&
+ echo yo >dirN/yo &&
+ git add dir* &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv dir1 combined &&
+ git mv dir2/* combined/ &&
+ git mv dir3/* combined/ &&
+ git mv dirN/* combined/ &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success C_LOCALE_OUTPUT '9e-check: N-to-1 whammo' '
+ (
+ cd 9e &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 >out &&
+ grep "CONFLICT (implicit dir rename): Cannot map more than one path to combined/yo" out >error_line &&
+ grep -q dir1/yo error_line &&
+ grep -q dir2/yo error_line &&
+ grep -q dir3/yo error_line &&
+ grep -q dirN/yo error_line &&
+
+ git ls-files -s >out &&
+ test_line_count = 16 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 2 out &&
+
+ git rev-parse >actual \
+ :0:combined/a :0:combined/b :0:combined/c \
+ :0:combined/d :0:combined/e :0:combined/f \
+ :0:combined/g :0:combined/h :0:combined/i \
+ :0:combined/j :0:combined/k :0:combined/l &&
+ git rev-parse >expect \
+ O:dir1/a O:dir1/b A:dir1/c \
+ O:dir2/d O:dir2/e A:dir2/f \
+ O:dir3/g O:dir3/h A:dir3/i \
+ O:dirN/j O:dirN/k A:dirN/l &&
+ test_cmp expect actual &&
+
+ git rev-parse >actual \
+ :0:dir1/yo :0:dir2/yo :0:dir3/yo :0:dirN/yo &&
+ git rev-parse >expect \
+ A:dir1/yo A:dir2/yo A:dir3/yo A:dirN/yo &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 9f, Renamed directory that only contained immediate subdirs
+# (Related to testcases 1e & 9g)
+# Commit O: goal/{a,b}/$more_files
+# Commit A: priority/{a,b}/$more_files
+# Commit B: goal/{a,b}/$more_files, goal/c
+# Expected: priority/{a,b}/$more_files, priority/c
+
+test_expect_success '9f-setup: Renamed directory that only contained immediate subdirs' '
+ test_create_repo 9f &&
+ (
+ cd 9f &&
+
+ mkdir -p goal/a &&
+ mkdir -p goal/b &&
+ echo foo >goal/a/foo &&
+ echo bar >goal/b/bar &&
+ echo baz >goal/b/baz &&
+ git add goal &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv goal/ priority &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo c >goal/c &&
+ git add goal/c &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '9f-check: Renamed directory that only contained immediate subdirs' '
+ (
+ cd 9f &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 4 out &&
+
+ git rev-parse >actual \
+ HEAD:priority/a/foo \
+ HEAD:priority/b/bar \
+ HEAD:priority/b/baz \
+ HEAD:priority/c &&
+ git rev-parse >expect \
+ O:goal/a/foo \
+ O:goal/b/bar \
+ O:goal/b/baz \
+ B:goal/c &&
+ test_cmp expect actual &&
+ test_must_fail git rev-parse HEAD:goal/c
+ )
+'
+
+# Testcase 9g, Renamed directory that only contained immediate subdirs, immediate subdirs renamed
+# (Related to testcases 1e & 9f)
+# Commit O: goal/{a,b}/$more_files
+# Commit A: priority/{alpha,bravo}/$more_files
+# Commit B: goal/{a,b}/$more_files, goal/c
+# Expected: priority/{alpha,bravo}/$more_files, priority/c
+
+test_expect_success '9g-setup: Renamed directory that only contained immediate subdirs, immediate subdirs renamed' '
+ test_create_repo 9g &&
+ (
+ cd 9g &&
+
+ mkdir -p goal/a &&
+ mkdir -p goal/b &&
+ echo foo >goal/a/foo &&
+ echo bar >goal/b/bar &&
+ echo baz >goal/b/baz &&
+ git add goal &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ mkdir priority &&
+ git mv goal/a/ priority/alpha &&
+ git mv goal/b/ priority/beta &&
+ rmdir goal/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo c >goal/c &&
+ git add goal/c &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_failure '9g-check: Renamed directory that only contained immediate subdirs, immediate subdirs renamed' '
+ (
+ cd 9g &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 4 out &&
+
+ git rev-parse >actual \
+ HEAD:priority/alpha/foo \
+ HEAD:priority/beta/bar \
+ HEAD:priority/beta/baz \
+ HEAD:priority/c &&
+ git rev-parse >expect \
+ O:goal/a/foo \
+ O:goal/b/bar \
+ O:goal/b/baz \
+ B:goal/c &&
+ test_cmp expect actual &&
+ test_must_fail git rev-parse HEAD:goal/c
+ )
+'
+
+# Testcase 9h, Avoid implicit rename if involved as source on other side
+# (Extremely closely related to testcase 3a)
+# Commit O: z/{b,c,d_1}
+# Commit A: z/{b,c,d_2}
+# Commit B: y/{b,c}, x/d_1
+# Expected: y/{b,c}, x/d_2
+# NOTE: If we applied the z/ -> y/ rename to z/d, then we'd end up with
+# a rename/rename(1to2) conflict (z/d -> y/d vs. x/d)
+test_expect_success '9h-setup: Avoid dir rename on merely modified path' '
+ test_create_repo 9h &&
+ (
+ cd 9h &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ printf "1\n2\n3\n4\n5\n6\n7\n8\nd\n" >z/d &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_tick &&
+ echo more >>z/d &&
+ git add z/d &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ mkdir y &&
+ mkdir x &&
+ git mv z/b y/ &&
+ git mv z/c y/ &&
+ git mv z/d x/ &&
+ rmdir z &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '9h-check: Avoid dir rename on merely modified path' '
+ (
+ cd 9h &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+
+ git rev-parse >actual \
+ HEAD:y/b HEAD:y/c HEAD:x/d &&
+ git rev-parse >expect \
+ O:z/b O:z/c A:z/d &&
+ test_cmp expect actual
+ )
+'
+
+###########################################################################
+# Rules suggested by section 9:
+#
+# If the other side of history did a directory rename to a path that your
+# side renamed away, then ignore that particular rename from the other
+# side of history for any implicit directory renames.
+###########################################################################
+
+###########################################################################
+# SECTION 10: Handling untracked files
+#
+# unpack_trees(), upon which the recursive merge algorithm is based, aborts
+# the operation if untracked or dirty files would be deleted or overwritten
+# by the merge. Unfortunately, unpack_trees() does not understand renames,
+# and if it doesn't abort, then it muddies up the working directory before
+# we even get to the point of detecting renames, so we need some special
+# handling, at least in the case of directory renames.
+###########################################################################
+
+# Testcase 10a, Overwrite untracked: normal rename/delete
+# Commit O: z/{b,c_1}
+# Commit A: z/b + untracked z/c + untracked z/d
+# Commit B: z/{b,d_1}
+# Expected: Aborted Merge +
+# ERROR_MSG(untracked working tree files would be overwritten by merge)
+
+test_expect_success '10a-setup: Overwrite untracked with normal rename/delete' '
+ test_create_repo 10a &&
+ (
+ cd 10a &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git rm z/c &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv z/c z/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '10a-check: Overwrite untracked with normal rename/delete' '
+ (
+ cd 10a &&
+
+ git checkout A^0 &&
+ echo very >z/c &&
+ echo important >z/d &&
+
+ test_must_fail git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep "The following untracked working tree files would be overwritten by merge" err &&
+
+ git ls-files -s >out &&
+ test_line_count = 1 out &&
+ git ls-files -o >out &&
+ test_line_count = 4 out &&
+
+ echo very >expect &&
+ test_cmp expect z/c &&
+
+ echo important >expect &&
+ test_cmp expect z/d &&
+
+ git rev-parse HEAD:z/b >actual &&
+ git rev-parse O:z/b >expect &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 10b, Overwrite untracked: dir rename + delete
+# Commit O: z/{b,c_1}
+# Commit A: y/b + untracked y/{c,d,e}
+# Commit B: z/{b,d_1,e}
+# Expected: Failed Merge; y/b + untracked y/c + untracked y/d on disk +
+# z/c_1 -> z/d_1 rename recorded at stage 3 for y/d +
+# ERROR_MSG(refusing to lose untracked file at 'y/d')
+
+test_expect_success '10b-setup: Overwrite untracked with dir rename + delete' '
+ test_create_repo 10b &&
+ (
+ cd 10b &&
+
+ mkdir z &&
+ echo b >z/b &&
+ echo c >z/c &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git rm z/c &&
+ git mv z/ y/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv z/c z/d &&
+ echo e >z/e &&
+ git add z/e &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '10b-check: Overwrite untracked with dir rename + delete' '
+ (
+ cd 10b &&
+
+ git checkout A^0 &&
+ echo very >y/c &&
+ echo important >y/d &&
+ echo contents >y/e &&
+
+ test_must_fail git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep "CONFLICT (rename/delete).*Version B\^0 of y/d left in tree at y/d~B\^0" out &&
+ test_i18ngrep "Error: Refusing to lose untracked file at y/e; writing to y/e~B\^0 instead" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+ git ls-files -u >out &&
+ test_line_count = 2 out &&
+ git ls-files -o >out &&
+ test_line_count = 5 out &&
+
+ git rev-parse >actual \
+ :0:y/b :3:y/d :3:y/e &&
+ git rev-parse >expect \
+ O:z/b O:z/c B:z/e &&
+ test_cmp expect actual &&
+
+ echo very >expect &&
+ test_cmp expect y/c &&
+
+ echo important >expect &&
+ test_cmp expect y/d &&
+
+ echo contents >expect &&
+ test_cmp expect y/e
+ )
+'
+
+# Testcase 10c, Overwrite untracked: dir rename/rename(1to2)
+# Commit O: z/{a,b}, x/{c,d}
+# Commit A: y/{a,b}, w/c, x/d + different untracked y/c
+# Commit B: z/{a,b,c}, x/d
+# Expected: Failed Merge; y/{a,b} + x/d + untracked y/c +
+# CONFLICT(rename/rename) x/c -> w/c vs y/c +
+# y/c~B^0 +
+# ERROR_MSG(Refusing to lose untracked file at y/c)
+
+test_expect_success '10c-setup: Overwrite untracked with dir rename/rename(1to2)' '
+ test_create_repo 10c &&
+ (
+ cd 10c &&
+
+ mkdir z x &&
+ echo a >z/a &&
+ echo b >z/b &&
+ echo c >x/c &&
+ echo d >x/d &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ mkdir w &&
+ git mv x/c w/c &&
+ git mv z/ y/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/c z/ &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '10c-check: Overwrite untracked with dir rename/rename(1to2)' '
+ (
+ cd 10c &&
+
+ git checkout A^0 &&
+ echo important >y/c &&
+
+ test_must_fail git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep "CONFLICT (rename/rename)" out &&
+ test_i18ngrep "Refusing to lose untracked file at y/c; adding as y/c~B\^0 instead" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 6 out &&
+ git ls-files -u >out &&
+ test_line_count = 3 out &&
+ git ls-files -o >out &&
+ test_line_count = 3 out &&
+
+ git rev-parse >actual \
+ :0:y/a :0:y/b :0:x/d :1:x/c :2:w/c :3:y/c &&
+ git rev-parse >expect \
+ O:z/a O:z/b O:x/d O:x/c O:x/c O:x/c &&
+ test_cmp expect actual &&
+
+ git hash-object y/c~B^0 >actual &&
+ git rev-parse O:x/c >expect &&
+ test_cmp expect actual &&
+
+ echo important >expect &&
+ test_cmp expect y/c
+ )
+'
+
+# Testcase 10d, Delete untracked w/ dir rename/rename(2to1)
+# Commit O: z/{a,b,c_1}, x/{d,e,f_2}
+# Commit A: y/{a,b}, x/{d,e,f_2,wham_1} + untracked y/wham
+# Commit B: z/{a,b,c_1,wham_2}, y/{d,e}
+# Expected: Failed Merge; y/{a,b,d,e} + untracked y/{wham,wham~B^0,wham~HEAD}+
+# CONFLICT(rename/rename) z/c_1 vs x/f_2 -> y/wham
+# ERROR_MSG(Refusing to lose untracked file at y/wham)
+
+test_expect_success '10d-setup: Delete untracked with dir rename/rename(2to1)' '
+ test_create_repo 10d &&
+ (
+ cd 10d &&
+
+ mkdir z x &&
+ echo a >z/a &&
+ echo b >z/b &&
+ echo c >z/c &&
+ echo d >x/d &&
+ echo e >x/e &&
+ echo f >x/f &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z/c x/wham &&
+ git mv z/ y/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/f z/wham &&
+ git mv x/ y/ &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '10d-check: Delete untracked with dir rename/rename(2to1)' '
+ (
+ cd 10d &&
+
+ git checkout A^0 &&
+ echo important >y/wham &&
+
+ test_must_fail git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep "CONFLICT (rename/rename)" out &&
+ test_i18ngrep "Refusing to lose untracked file at y/wham" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 6 out &&
+ git ls-files -u >out &&
+ test_line_count = 2 out &&
+ git ls-files -o >out &&
+ test_line_count = 4 out &&
+
+ git rev-parse >actual \
+ :0:y/a :0:y/b :0:y/d :0:y/e :2:y/wham :3:y/wham &&
+ git rev-parse >expect \
+ O:z/a O:z/b O:x/d O:x/e O:z/c O:x/f &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse :1:y/wham &&
+
+ echo important >expect &&
+ test_cmp expect y/wham &&
+
+ git hash-object >actual \
+ y/wham~B^0 y/wham~HEAD &&
+ git rev-parse >expect \
+ O:x/f O:z/c &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 10e, Does git complain about untracked file that's not in the way?
+# Commit O: z/{a,b}
+# Commit A: y/{a,b} + untracked z/c
+# Commit B: z/{a,b,c}
+# Expected: y/{a,b,c} + untracked z/c
+
+test_expect_success '10e-setup: Does git complain about untracked file that is not really in the way?' '
+ test_create_repo 10e &&
+ (
+ cd 10e &&
+
+ mkdir z &&
+ echo a >z/a &&
+ echo b >z/b &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z/ y/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo c >z/c &&
+ git add z/c &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_failure '10e-check: Does git complain about untracked file that is not really in the way?' '
+ (
+ cd 10e &&
+
+ git checkout A^0 &&
+ mkdir z &&
+ echo random >z/c &&
+
+ git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep ! "following untracked working tree files would be overwritten by merge" err &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 3 out &&
+
+ git rev-parse >actual \
+ :0:y/a :0:y/b :0:y/c &&
+ git rev-parse >expect \
+ O:z/a O:z/b B:z/c &&
+ test_cmp expect actual &&
+
+ echo random >expect &&
+ test_cmp expect z/c
+ )
+'
+
+###########################################################################
+# SECTION 11: Handling dirty (not up-to-date) files
+#
+# unpack_trees(), upon which the recursive merge algorithm is based, aborts
+# the operation if untracked or dirty files would be deleted or overwritten
+# by the merge. Unfortunately, unpack_trees() does not understand renames,
+# and if it doesn't abort, then it muddies up the working directory before
+# we even get to the point of detecting renames, so we need some special
+# handling. This was true even of normal renames, but there are additional
+# codepaths that need special handling with directory renames. Add
+# testcases for both renamed-by-directory-rename-detection and standard
+# rename cases.
+###########################################################################
+
+# Testcase 11a, Avoid losing dirty contents with simple rename
+# Commit O: z/{a,b_v1},
+# Commit A: z/{a,c_v1}, and z/c_v1 has uncommitted mods
+# Commit B: z/{a,b_v2}
+# Expected: ERROR_MSG(Refusing to lose dirty file at z/c) +
+# z/a, staged version of z/c has sha1sum matching B:z/b_v2,
+# z/c~HEAD with contents of B:z/b_v2,
+# z/c with uncommitted mods on top of A:z/c_v1
+
+test_expect_success '11a-setup: Avoid losing dirty contents with simple rename' '
+ test_create_repo 11a &&
+ (
+ cd 11a &&
+
+ mkdir z &&
+ echo a >z/a &&
+ test_seq 1 10 >z/b &&
+ git add z &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z/b z/c &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo 11 >>z/b &&
+ git add z/b &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '11a-check: Avoid losing dirty contents with simple rename' '
+ (
+ cd 11a &&
+
+ git checkout A^0 &&
+ echo stuff >>z/c &&
+
+ test_must_fail git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep "Refusing to lose dirty file at z/c" out &&
+
+ test_seq 1 10 >expected &&
+ echo stuff >>expected &&
+ test_cmp expected z/c &&
+
+ git ls-files -s >out &&
+ test_line_count = 2 out &&
+ git ls-files -u >out &&
+ test_line_count = 1 out &&
+ git ls-files -o >out &&
+ test_line_count = 4 out &&
+
+ git rev-parse >actual \
+ :0:z/a :2:z/c &&
+ git rev-parse >expect \
+ O:z/a B:z/b &&
+ test_cmp expect actual &&
+
+ git hash-object z/c~HEAD >actual &&
+ git rev-parse B:z/b >expect &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 11b, Avoid losing dirty file involved in directory rename
+# Commit O: z/a, x/{b,c_v1}
+# Commit A: z/{a,c_v1}, x/b, and z/c_v1 has uncommitted mods
+# Commit B: y/a, x/{b,c_v2}
+# Expected: y/{a,c_v2}, x/b, z/c_v1 with uncommitted mods untracked,
+# ERROR_MSG(Refusing to lose dirty file at z/c)
+
+
+test_expect_success '11b-setup: Avoid losing dirty file involved in directory rename' '
+ test_create_repo 11b &&
+ (
+ cd 11b &&
+
+ mkdir z x &&
+ echo a >z/a &&
+ echo b >x/b &&
+ test_seq 1 10 >x/c &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv x/c z/c &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv z y &&
+ echo 11 >>x/c &&
+ git add x/c &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '11b-check: Avoid losing dirty file involved in directory rename' '
+ (
+ cd 11b &&
+
+ git checkout A^0 &&
+ echo stuff >>z/c &&
+
+ git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep "Refusing to lose dirty file at z/c" out &&
+
+ grep -q stuff z/c &&
+ test_seq 1 10 >expected &&
+ echo stuff >>expected &&
+ test_cmp expected z/c &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -m >out &&
+ test_line_count = 0 out &&
+ git ls-files -o >out &&
+ test_line_count = 4 out &&
+
+ git rev-parse >actual \
+ :0:x/b :0:y/a :0:y/c &&
+ git rev-parse >expect \
+ O:x/b O:z/a B:x/c &&
+ test_cmp expect actual &&
+
+ git hash-object y/c >actual &&
+ git rev-parse B:x/c >expect &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 11c, Avoid losing not-up-to-date with rename + D/F conflict
+# Commit O: y/a, x/{b,c_v1}
+# Commit A: y/{a,c_v1}, x/b, and y/c_v1 has uncommitted mods
+# Commit B: y/{a,c/d}, x/{b,c_v2}
+# Expected: Abort_msg("following files would be overwritten by merge") +
+# y/c left untouched (still has uncommitted mods)
+
+test_expect_success '11c-setup: Avoid losing not-uptodate with rename + D/F conflict' '
+ test_create_repo 11c &&
+ (
+ cd 11c &&
+
+ mkdir y x &&
+ echo a >y/a &&
+ echo b >x/b &&
+ test_seq 1 10 >x/c &&
+ git add y x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv x/c y/c &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ mkdir y/c &&
+ echo d >y/c/d &&
+ echo 11 >>x/c &&
+ git add x/c y/c/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '11c-check: Avoid losing not-uptodate with rename + D/F conflict' '
+ (
+ cd 11c &&
+
+ git checkout A^0 &&
+ echo stuff >>y/c &&
+
+ test_must_fail git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep "following files would be overwritten by merge" err &&
+
+ grep -q stuff y/c &&
+ test_seq 1 10 >expected &&
+ echo stuff >>expected &&
+ test_cmp expected y/c &&
+
+ git ls-files -s >out &&
+ test_line_count = 3 out &&
+ git ls-files -u >out &&
+ test_line_count = 0 out &&
+ git ls-files -m >out &&
+ test_line_count = 1 out &&
+ git ls-files -o >out &&
+ test_line_count = 3 out
+ )
+'
+
+# Testcase 11d, Avoid losing not-up-to-date with rename + D/F conflict
+# Commit O: z/a, x/{b,c_v1}
+# Commit A: z/{a,c_v1}, x/b, and z/c_v1 has uncommitted mods
+# Commit B: y/{a,c/d}, x/{b,c_v2}
+# Expected: D/F: y/c_v2 vs y/c/d) +
+# Warning_Msg("Refusing to lose dirty file at z/c) +
+# y/{a,c~HEAD,c/d}, x/b, now-untracked z/c_v1 with uncommitted mods
+
+test_expect_success '11d-setup: Avoid losing not-uptodate with rename + D/F conflict' '
+ test_create_repo 11d &&
+ (
+ cd 11d &&
+
+ mkdir z x &&
+ echo a >z/a &&
+ echo b >x/b &&
+ test_seq 1 10 >x/c &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv x/c z/c &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv z y &&
+ mkdir y/c &&
+ echo d >y/c/d &&
+ echo 11 >>x/c &&
+ git add x/c y/c/d &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '11d-check: Avoid losing not-uptodate with rename + D/F conflict' '
+ (
+ cd 11d &&
+
+ git checkout A^0 &&
+ echo stuff >>z/c &&
+
+ test_must_fail git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep "Refusing to lose dirty file at z/c" out &&
+
+ grep -q stuff z/c &&
+ test_seq 1 10 >expected &&
+ echo stuff >>expected &&
+ test_cmp expected z/c
+
+ git ls-files -s >out &&
+ test_line_count = 4 out &&
+ git ls-files -u >out &&
+ test_line_count = 1 out &&
+ git ls-files -o >out &&
+ test_line_count = 5 out &&
+
+ git rev-parse >actual \
+ :0:x/b :0:y/a :0:y/c/d :3:y/c &&
+ git rev-parse >expect \
+ O:x/b O:z/a B:y/c/d B:x/c &&
+ test_cmp expect actual &&
+
+ git hash-object y/c~HEAD >actual &&
+ git rev-parse B:x/c >expect &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 11e, Avoid deleting not-up-to-date with dir rename/rename(1to2)/add
+# Commit O: z/{a,b}, x/{c_1,d}
+# Commit A: y/{a,b,c_2}, x/d, w/c_1, and y/c_2 has uncommitted mods
+# Commit B: z/{a,b,c_1}, x/d
+# Expected: Failed Merge; y/{a,b} + x/d +
+# CONFLICT(rename/rename) x/c_1 -> w/c_1 vs y/c_1 +
+# ERROR_MSG(Refusing to lose dirty file at y/c)
+# y/c~B^0 has O:x/c_1 contents
+# y/c~HEAD has A:y/c_2 contents
+# y/c has dirty file from before merge
+
+test_expect_success '11e-setup: Avoid deleting not-uptodate with dir rename/rename(1to2)/add' '
+ test_create_repo 11e &&
+ (
+ cd 11e &&
+
+ mkdir z x &&
+ echo a >z/a &&
+ echo b >z/b &&
+ echo c >x/c &&
+ echo d >x/d &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z/ y/ &&
+ echo different >y/c &&
+ mkdir w &&
+ git mv x/c w/ &&
+ git add y/c &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/c z/ &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '11e-check: Avoid deleting not-uptodate with dir rename/rename(1to2)/add' '
+ (
+ cd 11e &&
+
+ git checkout A^0 &&
+ echo mods >>y/c &&
+
+ test_must_fail git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep "CONFLICT (rename/rename)" out &&
+ test_i18ngrep "Refusing to lose dirty file at y/c" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 7 out &&
+ git ls-files -u >out &&
+ test_line_count = 4 out &&
+ git ls-files -o >out &&
+ test_line_count = 4 out &&
+
+ echo different >expected &&
+ echo mods >>expected &&
+ test_cmp expected y/c &&
+
+ git rev-parse >actual \
+ :0:y/a :0:y/b :0:x/d :1:x/c :2:w/c :2:y/c :3:y/c &&
+ git rev-parse >expect \
+ O:z/a O:z/b O:x/d O:x/c O:x/c A:y/c O:x/c &&
+ test_cmp expect actual &&
+
+ git hash-object >actual \
+ y/c~B^0 y/c~HEAD &&
+ git rev-parse >expect \
+ O:x/c A:y/c &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 11f, Avoid deleting not-up-to-date w/ dir rename/rename(2to1)
+# Commit O: z/{a,b}, x/{c_1,d_2}
+# Commit A: y/{a,b,wham_1}, x/d_2, except y/wham has uncommitted mods
+# Commit B: z/{a,b,wham_2}, x/c_1
+# Expected: Failed Merge; y/{a,b} + untracked y/{wham~B^0,wham~B^HEAD} +
+# y/wham with dirty changes from before merge +
+# CONFLICT(rename/rename) x/c vs x/d -> y/wham
+# ERROR_MSG(Refusing to lose dirty file at y/wham)
+
+test_expect_success '11f-setup: Avoid deleting not-uptodate with dir rename/rename(2to1)' '
+ test_create_repo 11f &&
+ (
+ cd 11f &&
+
+ mkdir z x &&
+ echo a >z/a &&
+ echo b >z/b &&
+ test_seq 1 10 >x/c &&
+ echo d >x/d &&
+ git add z x &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv z/ y/ &&
+ git mv x/c y/wham &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv x/d z/wham &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '11f-check: Avoid deleting not-uptodate with dir rename/rename(2to1)' '
+ (
+ cd 11f &&
+
+ git checkout A^0 &&
+ echo important >>y/wham &&
+
+ test_must_fail git merge -s recursive B^0 >out 2>err &&
+ test_i18ngrep "CONFLICT (rename/rename)" out &&
+ test_i18ngrep "Refusing to lose dirty file at y/wham" out &&
+
+ git ls-files -s >out &&
+ test_line_count = 4 out &&
+ git ls-files -u >out &&
+ test_line_count = 2 out &&
+ git ls-files -o >out &&
+ test_line_count = 4 out &&
+
+ test_seq 1 10 >expected &&
+ echo important >>expected &&
+ test_cmp expected y/wham &&
+
+ test_must_fail git rev-parse :1:y/wham &&
+ git hash-object >actual \
+ y/wham~B^0 y/wham~HEAD &&
+ git rev-parse >expect \
+ O:x/d O:x/c &&
+ test_cmp expect actual &&
+
+ git rev-parse >actual \
+ :0:y/a :0:y/b :2:y/wham :3:y/wham &&
+ git rev-parse >expect \
+ O:z/a O:z/b O:x/c O:x/d &&
+ test_cmp expect actual
+ )
+'
+
+###########################################################################
+# SECTION 12: Everything else
+#
+# Tests suggested by others. Tests added after implementation completed
+# and submitted. Grab bag.
+###########################################################################
+
+# Testcase 12a, Moving one directory hierarchy into another
+# (Related to testcase 9a)
+# Commit O: node1/{leaf1,leaf2}, node2/{leaf3,leaf4}
+# Commit A: node1/{leaf1,leaf2,node2/{leaf3,leaf4}}
+# Commit B: node1/{leaf1,leaf2,leaf5}, node2/{leaf3,leaf4,leaf6}
+# Expected: node1/{leaf1,leaf2,leaf5,node2/{leaf3,leaf4,leaf6}}
+
+test_expect_success '12a-setup: Moving one directory hierarchy into another' '
+ test_create_repo 12a &&
+ (
+ cd 12a &&
+
+ mkdir -p node1 node2 &&
+ echo leaf1 >node1/leaf1 &&
+ echo leaf2 >node1/leaf2 &&
+ echo leaf3 >node2/leaf3 &&
+ echo leaf4 >node2/leaf4 &&
+ git add node1 node2 &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv node2/ node1/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ echo leaf5 >node1/leaf5 &&
+ echo leaf6 >node2/leaf6 &&
+ git add node1 node2 &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '12a-check: Moving one directory hierarchy into another' '
+ (
+ cd 12a &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 6 out &&
+
+ git rev-parse >actual \
+ HEAD:node1/leaf1 HEAD:node1/leaf2 HEAD:node1/leaf5 \
+ HEAD:node1/node2/leaf3 \
+ HEAD:node1/node2/leaf4 \
+ HEAD:node1/node2/leaf6 &&
+ git rev-parse >expect \
+ O:node1/leaf1 O:node1/leaf2 B:node1/leaf5 \
+ O:node2/leaf3 \
+ O:node2/leaf4 \
+ B:node2/leaf6 &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 12b, Moving two directory hierarchies into each other
+# (Related to testcases 1c and 12c)
+# Commit O: node1/{leaf1, leaf2}, node2/{leaf3, leaf4}
+# Commit A: node1/{leaf1, leaf2, node2/{leaf3, leaf4}}
+# Commit B: node2/{leaf3, leaf4, node1/{leaf1, leaf2}}
+# Expected: node1/node2/node1/{leaf1, leaf2},
+# node2/node1/node2/{leaf3, leaf4}
+# NOTE: Without directory renames, we would expect
+# node2/node1/{leaf1, leaf2},
+# node1/node2/{leaf3, leaf4}
+# with directory rename detection, we note that
+# commit A renames node2/ -> node1/node2/
+# commit B renames node1/ -> node2/node1/
+# therefore, applying those directory renames to the initial result
+# (making all four paths experience a transitive renaming), yields
+# the expected result.
+#
+# You may ask, is it weird to have two directories rename each other?
+# To which, I can do no more than shrug my shoulders and say that
+# even simple rules give weird results when given weird inputs.
+
+test_expect_success '12b-setup: Moving one directory hierarchy into another' '
+ test_create_repo 12b &&
+ (
+ cd 12b &&
+
+ mkdir -p node1 node2 &&
+ echo leaf1 >node1/leaf1 &&
+ echo leaf2 >node1/leaf2 &&
+ echo leaf3 >node2/leaf3 &&
+ echo leaf4 >node2/leaf4 &&
+ git add node1 node2 &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv node2/ node1/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv node1/ node2/ &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '12b-check: Moving one directory hierarchy into another' '
+ (
+ cd 12b &&
+
+ git checkout A^0 &&
+
+ git merge -s recursive B^0 &&
+
+ git ls-files -s >out &&
+ test_line_count = 4 out &&
+
+ git rev-parse >actual \
+ HEAD:node1/node2/node1/leaf1 \
+ HEAD:node1/node2/node1/leaf2 \
+ HEAD:node2/node1/node2/leaf3 \
+ HEAD:node2/node1/node2/leaf4 &&
+ git rev-parse >expect \
+ O:node1/leaf1 \
+ O:node1/leaf2 \
+ O:node2/leaf3 \
+ O:node2/leaf4 &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 12c, Moving two directory hierarchies into each other w/ content merge
+# (Related to testcase 12b)
+# Commit O: node1/{ leaf1_1, leaf2_1}, node2/{leaf3_1, leaf4_1}
+# Commit A: node1/{ leaf1_2, leaf2_2, node2/{leaf3_2, leaf4_2}}
+# Commit B: node2/{node1/{leaf1_3, leaf2_3}, leaf3_3, leaf4_3}
+# Expected: Content merge conflicts for each of:
+# node1/node2/node1/{leaf1, leaf2},
+# node2/node1/node2/{leaf3, leaf4}
+# NOTE: This is *exactly* like 12c, except that every path is modified on
+# each side of the merge.
+
+test_expect_success '12c-setup: Moving one directory hierarchy into another w/ content merge' '
+ test_create_repo 12c &&
+ (
+ cd 12c &&
+
+ mkdir -p node1 node2 &&
+ printf "1\n2\n3\n4\n5\n6\n7\n8\nleaf1\n" >node1/leaf1 &&
+ printf "1\n2\n3\n4\n5\n6\n7\n8\nleaf2\n" >node1/leaf2 &&
+ printf "1\n2\n3\n4\n5\n6\n7\n8\nleaf3\n" >node2/leaf3 &&
+ printf "1\n2\n3\n4\n5\n6\n7\n8\nleaf4\n" >node2/leaf4 &&
+ git add node1 node2 &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv node2/ node1/ &&
+ for i in `git ls-files`; do echo side A >>$i; done &&
+ git add -u &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv node1/ node2/ &&
+ for i in `git ls-files`; do echo side B >>$i; done &&
+ git add -u &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '12c-check: Moving one directory hierarchy into another w/ content merge' '
+ (
+ cd 12c &&
+
+ git checkout A^0 &&
+
+ test_must_fail git merge -s recursive B^0 &&
+
+ git ls-files -u >out &&
+ test_line_count = 12 out &&
+
+ git rev-parse >actual \
+ :1:node1/node2/node1/leaf1 \
+ :1:node1/node2/node1/leaf2 \
+ :1:node2/node1/node2/leaf3 \
+ :1:node2/node1/node2/leaf4 \
+ :2:node1/node2/node1/leaf1 \
+ :2:node1/node2/node1/leaf2 \
+ :2:node2/node1/node2/leaf3 \
+ :2:node2/node1/node2/leaf4 \
+ :3:node1/node2/node1/leaf1 \
+ :3:node1/node2/node1/leaf2 \
+ :3:node2/node1/node2/leaf3 \
+ :3:node2/node1/node2/leaf4 &&
+ git rev-parse >expect \
+ O:node1/leaf1 \
+ O:node1/leaf2 \
+ O:node2/leaf3 \
+ O:node2/leaf4 \
+ A:node1/leaf1 \
+ A:node1/leaf2 \
+ A:node1/node2/leaf3 \
+ A:node1/node2/leaf4 \
+ B:node2/node1/leaf1 \
+ B:node2/node1/leaf2 \
+ B:node2/leaf3 \
+ B:node2/leaf4 &&
+ test_cmp expect actual
+ )
+'
+
+test_done
--- /dev/null
+#!/bin/sh
+
+test_description="merge cases"
+
+# The setup for all of them, pictorially, is:
+#
+# A
+# o
+# / \
+# O o ?
+# \ /
+# o
+# B
+#
+# To help make it easier to follow the flow of tests, they have been
+# divided into sections and each test will start with a quick explanation
+# of what commits O, A, and B contain.
+#
+# Notation:
+# z/{b,c} means files z/b and z/c both exist
+# x/d_1 means file x/d exists with content d1. (Purpose of the
+# underscore notation is to differentiate different
+# files that might be renamed into each other's paths.)
+
+. ./test-lib.sh
+
+
+###########################################################################
+# SECTION 1: Cases involving no renames (one side has subset of changes of
+# the other side)
+###########################################################################
+
+# Testcase 1a, Changes on A, subset of changes on B
+# Commit O: b_1
+# Commit A: b_2
+# Commit B: b_3
+# Expected: b_2
+
+test_expect_success '1a-setup: Modify(A)/Modify(B), change on B subset of A' '
+ test_create_repo 1a &&
+ (
+ cd 1a &&
+
+ test_write_lines 1 2 3 4 5 6 7 8 9 10 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 10.5 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '1a-check-L: Modify(A)/Modify(B), change on B subset of A' '
+ test_when_finished "git -C 1a reset --hard" &&
+ test_when_finished "git -C 1a clean -fd" &&
+ (
+ cd 1a &&
+
+ git checkout A^0 &&
+
+ test-tool chmtime =31337 b &&
+ test-tool chmtime -v +0 b >expected-mtime &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
+
+ test_i18ngrep "Skipped b" out &&
+ test_must_be_empty err &&
+
+ test-tool chmtime -v +0 b >actual-mtime &&
+ test_cmp expected-mtime actual-mtime &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 1 index_files &&
+
+ git rev-parse >actual HEAD:b &&
+ git rev-parse >expect A:b &&
+ test_cmp expect actual &&
+
+ git hash-object b >actual &&
+ git rev-parse A:b >expect &&
+ test_cmp expect actual
+ )
+'
+
+test_expect_success '1a-check-R: Modify(A)/Modify(B), change on B subset of A' '
+ test_when_finished "git -C 1a reset --hard" &&
+ test_when_finished "git -C 1a clean -fd" &&
+ (
+ cd 1a &&
+
+ git checkout B^0 &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive A^0 >out 2>err &&
+
+ test_i18ngrep "Auto-merging b" out &&
+ test_must_be_empty err &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 1 index_files &&
+
+ git rev-parse >actual HEAD:b &&
+ git rev-parse >expect A:b &&
+ test_cmp expect actual &&
+
+ git hash-object b >actual &&
+ git rev-parse A:b >expect &&
+ test_cmp expect actual
+ )
+'
+
+
+###########################################################################
+# SECTION 2: Cases involving basic renames
+###########################################################################
+
+# Testcase 2a, Changes on A, rename on B
+# Commit O: b_1
+# Commit A: b_2
+# Commit B: c_1
+# Expected: c_2
+
+test_expect_success '2a-setup: Modify(A)/rename(B)' '
+ test_create_repo 2a &&
+ (
+ cd 2a &&
+
+ test_seq 1 10 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_seq 1 11 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv b c &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '2a-check-L: Modify/rename, merge into modify side' '
+ test_when_finished "git -C 2a reset --hard" &&
+ test_when_finished "git -C 2a clean -fd" &&
+ (
+ cd 2a &&
+
+ git checkout A^0 &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
+
+ test_i18ngrep ! "Skipped c" out &&
+ test_must_be_empty err &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 1 index_files &&
+
+ git rev-parse >actual HEAD:c &&
+ git rev-parse >expect A:b &&
+ test_cmp expect actual &&
+
+ git hash-object c >actual &&
+ git rev-parse A:b >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse HEAD:b &&
+ test_path_is_missing b
+ )
+'
+
+test_expect_success '2a-check-R: Modify/rename, merge into rename side' '
+ test_when_finished "git -C 2a reset --hard" &&
+ test_when_finished "git -C 2a clean -fd" &&
+ (
+ cd 2a &&
+
+ git checkout B^0 &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive A^0 >out 2>err &&
+
+ test_i18ngrep ! "Skipped c" out &&
+ test_must_be_empty err &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 1 index_files &&
+
+ git rev-parse >actual HEAD:c &&
+ git rev-parse >expect A:b &&
+ test_cmp expect actual &&
+
+ git hash-object c >actual &&
+ git rev-parse A:b >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse HEAD:b &&
+ test_path_is_missing b
+ )
+'
+
+# Testcase 2b, Changed and renamed on A, subset of changes on B
+# Commit O: b_1
+# Commit A: c_2
+# Commit B: b_3
+# Expected: c_2
+
+test_expect_success '2b-setup: Rename+Mod(A)/Mod(B), B mods subset of A' '
+ test_create_repo 2b &&
+ (
+ cd 2b &&
+
+ test_write_lines 1 2 3 4 5 6 7 8 9 10 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 10.5 >b &&
+ git add b &&
+ git mv b c &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '2b-check-L: Rename+Mod(A)/Mod(B), B mods subset of A' '
+ test_when_finished "git -C 2b reset --hard" &&
+ test_when_finished "git -C 2b clean -fd" &&
+ (
+ cd 2b &&
+
+ git checkout A^0 &&
+
+ test-tool chmtime =31337 c &&
+ test-tool chmtime -v +0 c >expected-mtime &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
+
+ test_i18ngrep "Skipped c" out &&
+ test_must_be_empty err &&
+
+ test-tool chmtime -v +0 c >actual-mtime &&
+ test_cmp expected-mtime actual-mtime &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 1 index_files &&
+
+ git rev-parse >actual HEAD:c &&
+ git rev-parse >expect A:c &&
+ test_cmp expect actual &&
+
+ git hash-object c >actual &&
+ git rev-parse A:c >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse HEAD:b &&
+ test_path_is_missing b
+ )
+'
+
+test_expect_success '2b-check-R: Rename+Mod(A)/Mod(B), B mods subset of A' '
+ test_when_finished "git -C 2b reset --hard" &&
+ test_when_finished "git -C 2b clean -fd" &&
+ (
+ cd 2b &&
+
+ git checkout B^0 &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive A^0 >out 2>err &&
+
+ test_i18ngrep "Auto-merging c" out &&
+ test_must_be_empty err &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 1 index_files &&
+
+ git rev-parse >actual HEAD:c &&
+ git rev-parse >expect A:c &&
+ test_cmp expect actual &&
+
+ git hash-object c >actual &&
+ git rev-parse A:c >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse HEAD:b &&
+ test_path_is_missing b
+ )
+'
+
+# Testcase 2c, Changes on A, rename on B
+# Commit O: b_1
+# Commit A: b_2, c_3
+# Commit B: c_1
+# Expected: rename/add conflict c_2 vs c_3
+#
+# NOTE: Since A modified b_1->b_2, and B renamed b_1->c_1, the threeway
+# merge of those files should result in c_2. We then should have a
+# rename/add conflict between c_2 and c_3. However, if we note in
+# merge_content() that A had the right contents (b_2 has same
+# contents as c_2, just at a different name), and that A had the
+# right path present (c_3 existed) and thus decides that it can
+# skip the update, then we're in trouble. This test verifies we do
+# not make that particular mistake.
+
+test_expect_success '2c-setup: Modify b & add c VS rename b->c' '
+ test_create_repo 2c &&
+ (
+ cd 2c &&
+
+ test_seq 1 10 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_seq 1 11 >b &&
+ echo whatever >c &&
+ git add b c &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv b c &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '2c-check: Modify b & add c VS rename b->c' '
+ (
+ cd 2c &&
+
+ git checkout A^0 &&
+
+ GIT_MERGE_VERBOSITY=3 test_must_fail git merge -s recursive B^0 >out 2>err &&
+
+ test_i18ngrep "CONFLICT (rename/add): Rename b->c" out &&
+ test_i18ngrep ! "Skipped c" out &&
+ test_must_be_empty err
+
+ # FIXME: rename/add conflicts are horribly broken right now;
+ # when I get back to my patch series fixing it and
+ # rename/rename(2to1) conflicts to bring them in line with
+ # how add/add conflicts behave, then checks like the below
+ # could be added. But that patch series is waiting until
+ # the rename-directory-detection series lands, which this
+ # is part of. And in the mean time, I do not want to further
+ # enforce broken behavior. So for now, the main test is the
+ # one above that err is an empty file.
+
+ #git ls-files -s >index_files &&
+ #test_line_count = 2 index_files &&
+
+ #git rev-parse >actual :2:c :3:c &&
+ #git rev-parse >expect A:b A:c &&
+ #test_cmp expect actual &&
+
+ #git cat-file -p A:b >>merged &&
+ #git cat-file -p A:c >>merge-me &&
+ #>empty &&
+ #test_must_fail git merge-file \
+ # -L "Temporary merge branch 1" \
+ # -L "" \
+ # -L "Temporary merge branch 2" \
+ # merged empty merge-me &&
+ #sed -e "s/^\([<=>]\)/\1\1\1/" merged >merged-internal &&
+
+ #git hash-object c >actual &&
+ #git hash-object merged-internal >expect &&
+ #test_cmp expect actual &&
+
+ #test_path_is_missing b
+ )
+'
+
+
+###########################################################################
+# SECTION 3: Cases involving directory renames
+#
+# NOTE:
+# Directory renames only apply when one side renames a directory, and the
+# other side adds or renames a path into that directory. Applying the
+# directory rename to that new path creates a new pathname that didn't
+# exist on either side of history. Thus, it is impossible for the
+# merge contents to already be at the right path, so all of these checks
+# exist just to make sure that updates are not skipped.
+###########################################################################
+
+# Testcase 3a, Change + rename into dir foo on A, dir rename foo->bar on B
+# Commit O: bq_1, foo/whatever
+# Commit A: foo/{bq_2, whatever}
+# Commit B: bq_1, bar/whatever
+# Expected: bar/{bq_2, whatever}
+
+test_expect_success '3a-setup: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
+ test_create_repo 3a &&
+ (
+ cd 3a &&
+
+ mkdir foo &&
+ test_seq 1 10 >bq &&
+ test_write_lines a b c d e f g h i j k >foo/whatever &&
+ git add bq foo/whatever &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_seq 1 11 >bq &&
+ git add bq &&
+ git mv bq foo/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ git mv foo/ bar/ &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '3a-check-L: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
+ test_when_finished "git -C 3a reset --hard" &&
+ test_when_finished "git -C 3a clean -fd" &&
+ (
+ cd 3a &&
+
+ git checkout A^0 &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
+
+ test_i18ngrep ! "Skipped bar/bq" out &&
+ test_must_be_empty err &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 2 index_files &&
+
+ git rev-parse >actual HEAD:bar/bq HEAD:bar/whatever &&
+ git rev-parse >expect A:foo/bq A:foo/whatever &&
+ test_cmp expect actual &&
+
+ git hash-object bar/bq bar/whatever >actual &&
+ git rev-parse A:foo/bq A:foo/whatever >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse HEAD:bq HEAD:foo/bq &&
+ test_path_is_missing bq foo/bq foo/whatever
+ )
+'
+
+test_expect_success '3a-check-R: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
+ test_when_finished "git -C 3a reset --hard" &&
+ test_when_finished "git -C 3a clean -fd" &&
+ (
+ cd 3a &&
+
+ git checkout B^0 &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive A^0 >out 2>err &&
+
+ test_i18ngrep ! "Skipped bar/bq" out &&
+ test_must_be_empty err &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 2 index_files &&
+
+ git rev-parse >actual HEAD:bar/bq HEAD:bar/whatever &&
+ git rev-parse >expect A:foo/bq A:foo/whatever &&
+ test_cmp expect actual &&
+
+ git hash-object bar/bq bar/whatever >actual &&
+ git rev-parse A:foo/bq A:foo/whatever >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse HEAD:bq HEAD:foo/bq &&
+ test_path_is_missing bq foo/bq foo/whatever
+ )
+'
+
+# Testcase 3b, rename into dir foo on A, dir rename foo->bar + change on B
+# Commit O: bq_1, foo/whatever
+# Commit A: foo/{bq_1, whatever}
+# Commit B: bq_2, bar/whatever
+# Expected: bar/{bq_2, whatever}
+
+test_expect_success '3b-setup: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
+ test_create_repo 3b &&
+ (
+ cd 3b &&
+
+ mkdir foo &&
+ test_seq 1 10 >bq &&
+ test_write_lines a b c d e f g h i j k >foo/whatever &&
+ git add bq foo/whatever &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ git mv bq foo/ &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ test_seq 1 11 >bq &&
+ git add bq &&
+ git mv foo/ bar/ &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '3b-check-L: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
+ test_when_finished "git -C 3b reset --hard" &&
+ test_when_finished "git -C 3b clean -fd" &&
+ (
+ cd 3b &&
+
+ git checkout A^0 &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
+
+ test_i18ngrep ! "Skipped bar/bq" out &&
+ test_must_be_empty err &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 2 index_files &&
+
+ git rev-parse >actual HEAD:bar/bq HEAD:bar/whatever &&
+ git rev-parse >expect B:bq A:foo/whatever &&
+ test_cmp expect actual &&
+
+ git hash-object bar/bq bar/whatever >actual &&
+ git rev-parse B:bq A:foo/whatever >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse HEAD:bq HEAD:foo/bq &&
+ test_path_is_missing bq foo/bq foo/whatever
+ )
+'
+
+test_expect_success '3b-check-R: bq_1->foo/bq_2 on A, foo/->bar/ on B' '
+ test_when_finished "git -C 3b reset --hard" &&
+ test_when_finished "git -C 3b clean -fd" &&
+ (
+ cd 3b &&
+
+ git checkout B^0 &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive A^0 >out 2>err &&
+
+ test_i18ngrep ! "Skipped bar/bq" out &&
+ test_must_be_empty err &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 2 index_files &&
+
+ git rev-parse >actual HEAD:bar/bq HEAD:bar/whatever &&
+ git rev-parse >expect B:bq A:foo/whatever &&
+ test_cmp expect actual &&
+
+ git hash-object bar/bq bar/whatever >actual &&
+ git rev-parse B:bq A:foo/whatever >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse HEAD:bq HEAD:foo/bq &&
+ test_path_is_missing bq foo/bq foo/whatever
+ )
+'
+
+###########################################################################
+# SECTION 4: Cases involving dirty changes
+###########################################################################
+
+# Testcase 4a, Changed on A, subset of changes on B, locally modified
+# Commit O: b_1
+# Commit A: b_2
+# Commit B: b_3
+# Working copy: b_4
+# Expected: b_2 for merge, b_4 in working copy
+
+test_expect_success '4a-setup: Change on A, change on B subset of A, dirty mods present' '
+ test_create_repo 4a &&
+ (
+ cd 4a &&
+
+ test_write_lines 1 2 3 4 5 6 7 8 9 10 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 10.5 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+# NOTE: For as long as we continue using unpack_trees() without index_only
+# set to true, it will error out on a case like this claiming the the locally
+# modified file would be overwritten by the merge. Getting this testcase
+# correct requires doing the merge in-memory first, then realizing that no
+# updates to the file are necessary, and thus that we can just leave the path
+# alone.
+test_expect_failure '4a-check: Change on A, change on B subset of A, dirty mods present' '
+ test_when_finished "git -C 4a reset --hard" &&
+ test_when_finished "git -C 4a clean -fd" &&
+ (
+ cd 4a &&
+
+ git checkout A^0 &&
+ echo "File rewritten" >b &&
+
+ test-tool chmtime =31337 b &&
+ test-tool chmtime -v +0 b >expected-mtime &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
+
+ test_i18ngrep "Skipped b" out &&
+ test_must_be_empty err &&
+
+ test-tool chmtime -v +0 b >actual-mtime &&
+ test_cmp expected-mtime actual-mtime &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 1 index_files &&
+
+ git rev-parse >actual :0:b &&
+ git rev-parse >expect A:b &&
+ test_cmp expect actual &&
+
+ git hash-object b >actual &&
+ echo "File rewritten" | git hash-object --stdin >expect &&
+ test_cmp expect actual
+ )
+'
+
+# Testcase 4b, Changed+renamed on A, subset of changes on B, locally modified
+# Commit O: b_1
+# Commit A: c_2
+# Commit B: b_3
+# Working copy: c_4
+# Expected: c_2
+
+test_expect_success '4b-setup: Rename+Mod(A)/Mod(B), change on B subset of A, dirty mods present' '
+ test_create_repo 4b &&
+ (
+ cd 4b &&
+
+ test_write_lines 1 2 3 4 5 6 7 8 9 10 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "O" &&
+
+ git branch O &&
+ git branch A &&
+ git branch B &&
+
+ git checkout A &&
+ test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 10.5 >b &&
+ git add b &&
+ git mv b c &&
+ test_tick &&
+ git commit -m "A" &&
+
+ git checkout B &&
+ test_write_lines 1 2 3 4 5 5.5 6 7 8 9 10 >b &&
+ git add b &&
+ test_tick &&
+ git commit -m "B"
+ )
+'
+
+test_expect_success '4b-check: Rename+Mod(A)/Mod(B), change on B subset of A, dirty mods present' '
+ test_when_finished "git -C 4b reset --hard" &&
+ test_when_finished "git -C 4b clean -fd" &&
+ (
+ cd 4b &&
+
+ git checkout A^0 &&
+ echo "File rewritten" >c &&
+
+ test-tool chmtime =31337 c &&
+ test-tool chmtime -v +0 c >expected-mtime &&
+
+ GIT_MERGE_VERBOSITY=3 git merge -s recursive B^0 >out 2>err &&
+
+ test_i18ngrep "Skipped c" out &&
+ test_must_be_empty err &&
+
+ test-tool chmtime -v +0 c >actual-mtime &&
+ test_cmp expected-mtime actual-mtime &&
+
+ git ls-files -s >index_files &&
+ test_line_count = 1 index_files &&
+
+ git rev-parse >actual :0:c &&
+ git rev-parse >expect A:c &&
+ test_cmp expect actual &&
+
+ git hash-object c >actual &&
+ echo "File rewritten" | git hash-object --stdin >expect &&
+ test_cmp expect actual &&
+
+ test_must_fail git rev-parse HEAD:b &&
+ test_path_is_missing b
+ )
+'
+
+test_done
#
test_description='Tests replace refs functionality'
-exec </dev/null
-
. ./test-lib.sh
. "$TEST_DIRECTORY/lib-gpg.sh"
git replace -d $HASH10
'
+test_expect_success '--convert-graft-file' '
+ git checkout -b with-graft-file &&
+ test_commit root2 &&
+ git reset --hard root2^ &&
+ test_commit root1 &&
+ test_commit after-root1 &&
+ test_tick &&
+ git merge -m merge-root2 root2 &&
+
+ : add and convert graft file &&
+ printf "%s\n%s %s\n\n# comment\n%s\n" \
+ $(git rev-parse HEAD^^ HEAD^ HEAD^^ HEAD^2) \
+ >.git/info/grafts &&
+ git replace --convert-graft-file &&
+ test_path_is_missing .git/info/grafts &&
+
+ : verify that the history is now "grafted" &&
+ git rev-list HEAD >out &&
+ test_line_count = 4 out &&
+
+ : create invalid graft file and verify that it is not deleted &&
+ test_when_finished "rm -f .git/info/grafts" &&
+ echo $EMPTY_BLOB $EMPTY_TREE >.git/info/grafts &&
+ test_must_fail git replace --convert-graft-file 2>err &&
+ test_i18ngrep "$EMPTY_BLOB $EMPTY_TREE" err &&
+ test_i18ngrep "$EMPTY_BLOB $EMPTY_TREE" .git/info/grafts
+'
+
test_done
. ./test-lib.sh
+test_expect_success 'setup' '
+ # do not let the amount of physical memory affects gc
+ # behavior, make sure we always pack everything to one pack by
+ # default
+ git config gc.bigPackThreshold 2g
+'
+
test_expect_success 'gc empty repository' '
git gc
'
)
'
+test_expect_success 'gc --keep-largest-pack' '
+ test_create_repo keep-pack &&
+ (
+ cd keep-pack &&
+ test_commit one &&
+ test_commit two &&
+ test_commit three &&
+ git gc &&
+ ( cd .git/objects/pack && ls *.pack ) >pack-list &&
+ test_line_count = 1 pack-list &&
+ BASE_PACK=.git/objects/pack/pack-*.pack &&
+ test_commit four &&
+ git repack -d &&
+ test_commit five &&
+ git repack -d &&
+ ( cd .git/objects/pack && ls *.pack ) >pack-list &&
+ test_line_count = 3 pack-list &&
+ git gc --keep-largest-pack &&
+ ( cd .git/objects/pack && ls *.pack ) >pack-list &&
+ test_line_count = 2 pack-list &&
+ test_path_is_file $BASE_PACK &&
+ git fsck
+ )
+'
+
test_expect_success 'auto gc with too many loose objects does not attempt to create bitmaps' '
test_config gc.auto 3 &&
test_config gc.autodetach false &&
'
test_expect_success 'listing tags in column' '
- COLUMNS=40 git tag -l --column=row >actual &&
+ COLUMNS=41 git tag -l --column=row >actual &&
cat >expected <<\EOF &&
a1 aa1 cba t210 t211
v0.2.1 v1.0 v1.0.1 v1.1.3
'
done
-if echo 'echo space > "$1"' > "e space.sh"
-then
- # FS supports spaces in filenames
- test_set_prereq SPACES_IN_FILENAMES
-fi
-
-test_expect_success SPACES_IN_FILENAMES 'editor with a space' '
-
+test_expect_success 'editor with a space' '
+ echo "echo space >\$1" >"e space.sh" &&
chmod a+x "e space.sh" &&
GIT_EDITOR="./e\ space.sh" git commit --amend &&
test space = "$(git show -s --pretty=format:%s)"
'
unset GIT_EDITOR
-test_expect_success SPACES_IN_FILENAMES 'core.editor with a space' '
+test_expect_success 'core.editor with a space' '
git config core.editor \"./e\ space.sh\" &&
git commit --amend &&
test_cmp empty untracked
'
+test_create_repo parent &&
+test_commit -C parent one
+
+test_expect_success 'redirected submodule add does not show progress' '
+ git -C addtest submodule add "file://$submodurl/parent" submod-redirected \
+ 2>err &&
+ ! grep % err &&
+ test_i18ngrep ! "Checking connectivity" err
+'
+
+test_expect_success 'redirected submodule add --progress does show progress' '
+ git -C addtest submodule add --progress "file://$submodurl/parent" \
+ submod-redirected-progress 2>err && \
+ grep % err
+'
+
test_expect_success 'submodule add to .gitignored path fails' '
(
cd addtest-ignore &&
test_alternate_is_used super/.git/modules/sub/objects/info/alternates super/sub
'
+test_expect_success 'submodule add --reference with --dissociate does not use alternates' '
+ (
+ cd super &&
+ git submodule add --reference ../B --dissociate "file://$base_dir/A" sub-dissociate &&
+ git commit -m B-super-added &&
+ git repack -ad
+ ) &&
+ test_path_is_missing super/.git/modules/sub-dissociate/objects/info/alternates
+'
+
test_expect_success 'that reference gets used with add' '
(
cd super/sub &&
test_alternate_is_used super-clone/.git/modules/sub/objects/info/alternates super-clone/sub
'
+test_expect_success 'updating superproject with --dissociate does not keep alternates' '
+ test_when_finished "rm -rf super-clone" &&
+ git clone super super-clone &&
+ git -C super-clone submodule update --init --reference ../B --dissociate &&
+ test_path_is_missing super-clone/.git/modules/sub/objects/info/alternates
+'
+
test_expect_success 'submodules use alternates when cloning a superproject' '
test_when_finished "rm -rf super-clone" &&
git clone --reference super --recursive super super-clone &&
--- /dev/null
+#!/bin/sh
+
+test_description='check handling of .. in submodule names
+
+Exercise the name-checking function on a variety of names, and then give a
+real-world setup that confirms we catch this in practice.
+'
+. ./test-lib.sh
+. "$TEST_DIRECTORY"/lib-pack.sh
+
+test_expect_success 'check names' '
+ cat >expect <<-\EOF &&
+ valid
+ valid/with/paths
+ EOF
+
+ git submodule--helper check-name >actual <<-\EOF &&
+ valid
+ valid/with/paths
+
+ ../foo
+ /../foo
+ ..\foo
+ \..\foo
+ foo/..
+ foo/../
+ foo\..
+ foo\..\
+ foo/../bar
+ EOF
+
+ test_cmp expect actual
+'
+
+test_expect_success 'create innocent subrepo' '
+ git init innocent &&
+ git -C innocent commit --allow-empty -m foo
+'
+
+test_expect_success 'submodule add refuses invalid names' '
+ test_must_fail \
+ git submodule add --name ../../modules/evil "$PWD/innocent" evil
+'
+
+test_expect_success 'add evil submodule' '
+ git submodule add "$PWD/innocent" evil &&
+
+ mkdir modules &&
+ cp -r .git/modules/evil modules &&
+ write_script modules/evil/hooks/post-checkout <<-\EOF &&
+ echo >&2 "RUNNING POST CHECKOUT"
+ EOF
+
+ git config -f .gitmodules submodule.evil.update checkout &&
+ git config -f .gitmodules --rename-section \
+ submodule.evil submodule.../../modules/evil &&
+ git add modules &&
+ git commit -am evil
+'
+
+# This step seems like it shouldn't be necessary, since the payload is
+# contained entirely in the evil submodule. But due to the vagaries of the
+# submodule code, checking out the evil module will fail unless ".git/modules"
+# exists. Adding another submodule (with a name that sorts before "evil") is an
+# easy way to make sure this is the case in the victim clone.
+test_expect_success 'add other submodule' '
+ git submodule add "$PWD/innocent" another-module &&
+ git add another-module &&
+ git commit -am another
+'
+
+test_expect_success 'clone evil superproject' '
+ git clone --recurse-submodules . victim >output 2>&1 &&
+ ! grep "RUNNING POST CHECKOUT" output
+'
+
+test_expect_success 'fsck detects evil superproject' '
+ test_must_fail git fsck
+'
+
+test_expect_success 'transfer.fsckObjects detects evil superproject (unpack)' '
+ rm -rf dst.git &&
+ git init --bare dst.git &&
+ git -C dst.git config transfer.fsckObjects true &&
+ test_must_fail git push dst.git HEAD
+'
+
+test_expect_success 'transfer.fsckObjects detects evil superproject (index)' '
+ rm -rf dst.git &&
+ git init --bare dst.git &&
+ git -C dst.git config transfer.fsckObjects true &&
+ git -C dst.git config transfer.unpackLimit 1 &&
+ test_must_fail git push dst.git HEAD
+'
+
+# Normally our packs contain commits followed by trees followed by blobs. This
+# reverses the order, which requires backtracking to find the context of a
+# blob. We'll start with a fresh gitmodules-only tree to make it simpler.
+test_expect_success 'create oddly ordered pack' '
+ git checkout --orphan odd &&
+ git rm -rf --cached . &&
+ git add .gitmodules &&
+ git commit -m odd &&
+ {
+ pack_header 3 &&
+ pack_obj $(git rev-parse HEAD:.gitmodules) &&
+ pack_obj $(git rev-parse HEAD^{tree}) &&
+ pack_obj $(git rev-parse HEAD)
+ } >odd.pack &&
+ pack_trailer odd.pack
+'
+
+test_expect_success 'transfer.fsckObjects handles odd pack (unpack)' '
+ rm -rf dst.git &&
+ git init --bare dst.git &&
+ test_must_fail git -C dst.git unpack-objects --strict <odd.pack
+'
+
+test_expect_success 'transfer.fsckObjects handles odd pack (index)' '
+ rm -rf dst.git &&
+ git init --bare dst.git &&
+ test_must_fail git -C dst.git index-pack --strict --stdin <odd.pack
+'
+
+test_expect_success 'fsck detects symlinked .gitmodules file' '
+ git init symlink &&
+ (
+ cd symlink &&
+
+ # Make the tree directly to avoid index restrictions.
+ #
+ # Because symlinks store the target as a blob, choose
+ # a pathname that could be parsed as a .gitmodules file
+ # to trick naive non-symlink-aware checking.
+ tricky="[foo]bar=true" &&
+ content=$(git hash-object -w ../.gitmodules) &&
+ target=$(printf "$tricky" | git hash-object -w --stdin) &&
+ tree=$(
+ {
+ printf "100644 blob $content\t$tricky\n" &&
+ printf "120000 blob $target\t.gitmodules\n"
+ } | git mktree
+ ) &&
+ commit=$(git commit-tree $tree) &&
+
+ # Check not only that we fail, but that it is due to the
+ # symlink detector; this grep string comes from the config
+ # variable name and will not be translated.
+ test_must_fail git fsck 2>output &&
+ grep gitmodulesSymlink output
+ )
+'
+
+test_done
--- /dev/null
+#!/bin/sh
+
+test_description='git status rename detection options'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+ echo 1 >original &&
+ git add . &&
+ git commit -m"Adding original file." &&
+ mv original renamed &&
+ echo 2 >> renamed &&
+ git add . &&
+ cat >.gitignore <<-\EOF
+ .gitignore
+ expect*
+ actual*
+ EOF
+'
+
+test_expect_success 'status no-options' '
+ git status >actual &&
+ test_i18ngrep "renamed:" actual
+'
+
+test_expect_success 'status --no-renames' '
+ git status --no-renames >actual &&
+ test_i18ngrep "deleted:" actual &&
+ test_i18ngrep "new file:" actual
+'
+
+test_expect_success 'status.renames inherits from diff.renames false' '
+ git -c diff.renames=false status >actual &&
+ test_i18ngrep "deleted:" actual &&
+ test_i18ngrep "new file:" actual
+'
+
+test_expect_success 'status.renames inherits from diff.renames true' '
+ git -c diff.renames=true status >actual &&
+ test_i18ngrep "renamed:" actual
+'
+
+test_expect_success 'status.renames overrides diff.renames false' '
+ git -c diff.renames=true -c status.renames=false status >actual &&
+ test_i18ngrep "deleted:" actual &&
+ test_i18ngrep "new file:" actual
+'
+
+test_expect_success 'status.renames overrides from diff.renames true' '
+ git -c diff.renames=false -c status.renames=true status >actual &&
+ test_i18ngrep "renamed:" actual
+'
+
+test_expect_success 'status status.renames=false' '
+ git -c status.renames=false status >actual &&
+ test_i18ngrep "deleted:" actual &&
+ test_i18ngrep "new file:" actual
+'
+
+test_expect_success 'status status.renames=true' '
+ git -c status.renames=true status >actual &&
+ test_i18ngrep "renamed:" actual
+'
+
+test_expect_success 'commit honors status.renames=false' '
+ git -c status.renames=false commit --dry-run >actual &&
+ test_i18ngrep "deleted:" actual &&
+ test_i18ngrep "new file:" actual
+'
+
+test_expect_success 'commit honors status.renames=true' '
+ git -c status.renames=true commit --dry-run >actual &&
+ test_i18ngrep "renamed:" actual
+'
+
+test_expect_success 'status config overridden' '
+ git -c status.renames=true status --no-renames >actual &&
+ test_i18ngrep "deleted:" actual &&
+ test_i18ngrep "new file:" actual
+'
+
+test_expect_success 'status score=100%' '
+ git status -M=100% >actual &&
+ test_i18ngrep "deleted:" actual &&
+ test_i18ngrep "new file:" actual &&
+
+ git status --find-rename=100% >actual &&
+ test_i18ngrep "deleted:" actual &&
+ test_i18ngrep "new file:" actual
+'
+
+test_expect_success 'status score=01%' '
+ git status -M=01% >actual &&
+ test_i18ngrep "renamed:" actual &&
+
+ git status --find-rename=01% >actual &&
+ test_i18ngrep "renamed:" actual
+'
+
+test_expect_success 'copies not overridden by find-rename' '
+ cp renamed copy &&
+ git add copy &&
+
+ git -c status.renames=copies status -M=01% >actual &&
+ test_i18ngrep "copied:" actual &&
+ test_i18ngrep "renamed:" actual &&
+
+ git -c status.renames=copies status --find-rename=01% >actual &&
+ test_i18ngrep "copied:" actual &&
+ test_i18ngrep "renamed:" actual
+'
+
+test_done
test_cmp important c1.c
'
-test_expect_failure 'will not overwrite unstaged changes in renamed file' '
+test_expect_success 'will not overwrite unstaged changes in renamed file' '
git reset --hard c1 &&
git mv c1.c other.c &&
git commit -m rename &&
. ./test-lib.sh
+commit_and_pack() {
+ test_commit "$@" >/dev/null &&
+ SHA1=$(git pack-objects --all --unpacked --incremental .git/objects/pack/pack </dev/null) &&
+ echo pack-${SHA1}.pack
+}
+
test_expect_success 'objects in packs marked .keep are not repacked' '
echo content1 > file1 &&
echo content2 > file2 &&
git reflog expire --expire=$test_tick --expire-unreachable=$test_tick --all &&
git repack -a -d &&
git cat-file -t $H1
- '
+'
+
+test_expect_success 'repack --keep-pack' '
+ test_create_repo keep-pack &&
+ (
+ cd keep-pack &&
+ P1=$(commit_and_pack 1) &&
+ P2=$(commit_and_pack 2) &&
+ P3=$(commit_and_pack 3) &&
+ P4=$(commit_and_pack 4) &&
+ ls .git/objects/pack/*.pack >old-counts &&
+ test_line_count = 4 old-counts &&
+ git repack -a -d --keep-pack $P1 --keep-pack $P4 &&
+ ls .git/objects/pack/*.pack >new-counts &&
+ grep -q $P1 new-counts &&
+ grep -q $P4 new-counts &&
+ test_line_count = 3 new-counts &&
+ git fsck
+ )
+'
test_done
--- /dev/null
+#!/bin/sh
+
+test_description='colored git blame'
+. ./test-lib.sh
+
+PROG='git blame -c'
+. "$TEST_DIRECTORY"/annotate-tests.sh
+
+test_expect_success 'colored blame colors contiguous lines' '
+ git -c color.blame.repeatedLines=yellow blame --color-lines --abbrev=12 hello.c >actual.raw &&
+ git -c color.blame.repeatedLines=yellow -c blame.coloring=repeatedLines blame --abbrev=12 hello.c >actual.raw.2 &&
+ test_cmp actual.raw actual.raw.2 &&
+ test_decode_color <actual.raw >actual &&
+ grep "<YELLOW>" <actual >darkened &&
+ grep "(F" darkened > F.expect &&
+ grep "(H" darkened > H.expect &&
+ test_line_count = 2 F.expect &&
+ test_line_count = 3 H.expect
+'
+
+test_expect_success 'color by age consistently colors old code' '
+ git blame --color-by-age hello.c >actual.raw &&
+ git -c blame.coloring=highlightRecent blame hello.c >actual.raw.2 &&
+ test_cmp actual.raw actual.raw.2 &&
+ test_decode_color <actual.raw >actual &&
+ grep "<BLUE>" <actual >colored &&
+ test_line_count = 10 colored
+'
+
+test_expect_success 'blame color by age: new code is different' '
+ cat >>hello.c <<-EOF &&
+ void qfunc();
+ EOF
+ git add hello.c &&
+ GIT_AUTHOR_DATE="" git commit -m "new commit" &&
+
+ git -c color.blame.highlightRecent="yellow,1 month ago, cyan" blame --color-by-age hello.c >actual.raw &&
+ test_decode_color <actual.raw >actual &&
+
+ grep "<YELLOW>" <actual >colored &&
+ test_line_count = 10 colored &&
+
+ grep "<CYAN>" <actual >colored &&
+ test_line_count = 1 colored &&
+ grep qfunc colored
+'
+
+test_done
then
printf '%s\n' "$2" >expected
else
- sed -e 's/Z$//' >expected
+ sed -e 's/Z$//' |sort >expected
fi &&
run_completion "$1" &&
- test_cmp expected out
+ sort out >out_sorted &&
+ test_cmp expected out_sorted
}
# Test __gitcomp.
test_cmp expected "$actual"
'
+
+test_expect_success '__git_dequote - plain unquoted word' '
+ __git_dequote unquoted-word &&
+ verbose test unquoted-word = "$dequoted_word"
+'
+
+# input: b\a\c\k\'\\\"s\l\a\s\h\es
+# expected: back'\"slashes
+test_expect_success '__git_dequote - backslash escaped' '
+ __git_dequote "b\a\c\k\\'\''\\\\\\\"s\l\a\s\h\es" &&
+ verbose test "back'\''\\\"slashes" = "$dequoted_word"
+'
+
+# input: sin'gle\' '"quo'ted
+# expected: single\ "quoted
+test_expect_success '__git_dequote - single quoted' '
+ __git_dequote "'"sin'gle\\\\' '\\\"quo'ted"'" &&
+ verbose test '\''single\ "quoted'\'' = "$dequoted_word"
+'
+
+# input: dou"ble\\" "\"\quot"ed
+# expected: double\ "\quoted
+test_expect_success '__git_dequote - double quoted' '
+ __git_dequote '\''dou"ble\\" "\"\quot"ed'\'' &&
+ verbose test '\''double\ "\quoted'\'' = "$dequoted_word"
+'
+
+# input: 'open single quote
+test_expect_success '__git_dequote - open single quote' '
+ __git_dequote "'\''open single quote" &&
+ verbose test "open single quote" = "$dequoted_word"
+'
+
+# input: "open double quote
+test_expect_success '__git_dequote - open double quote' '
+ __git_dequote "\"open double quote" &&
+ verbose test "open double quote" = "$dequoted_word"
+'
+
+
test_expect_success '__gitcomp_direct - puts everything into COMPREPLY as-is' '
sed -e "s/Z$//g" >expected <<-EOF &&
with-trailing-space Z
git remote remove other
'
+
+test_path_completion ()
+{
+ test $# = 2 || error "bug in the test script: not 2 parameters to test_path_completion"
+
+ local cur="$1" expected="$2"
+ echo "$expected" >expected &&
+ (
+ # In the following tests calling this function we only
+ # care about how __git_complete_index_file() deals with
+ # unusual characters in path names. By requesting only
+ # untracked files we dont have to bother adding any
+ # paths to the index in those tests.
+ __git_complete_index_file --others &&
+ print_comp
+ ) &&
+ test_cmp expected out
+}
+
+test_expect_success 'setup for path completion tests' '
+ mkdir simple-dir \
+ "spaces in dir" \
+ árvíztűrő &&
+ touch simple-dir/simple-file \
+ "spaces in dir/spaces in file" \
+ "árvíztűrő/Сайн яваарай" &&
+ if test_have_prereq !MINGW &&
+ mkdir BS\\dir \
+ '$'separators\034in\035dir'' &&
+ touch BS\\dir/DQ\"file \
+ '$'separators\034in\035dir/sep\036in\037file''
+ then
+ test_set_prereq FUNNYNAMES
+ else
+ rm -rf BS\\dir '$'separators\034in\035dir''
+ fi
+'
+
+test_expect_success '__git_complete_index_file - simple' '
+ test_path_completion simple simple-dir && # Bash is supposed to
+ # add the trailing /.
+ test_path_completion simple-dir/simple simple-dir/simple-file
+'
+
+test_expect_success \
+ '__git_complete_index_file - escaped characters on cmdline' '
+ test_path_completion spac "spaces in dir" && # Bash will turn this
+ # into "spaces\ in\ dir"
+ test_path_completion "spaces\\ i" \
+ "spaces in dir" &&
+ test_path_completion "spaces\\ in\\ dir/s" \
+ "spaces in dir/spaces in file" &&
+ test_path_completion "spaces\\ in\\ dir/spaces\\ i" \
+ "spaces in dir/spaces in file"
+'
+
+test_expect_success \
+ '__git_complete_index_file - quoted characters on cmdline' '
+ # Testing with an opening but without a corresponding closing
+ # double quote is important.
+ test_path_completion \"spac "spaces in dir" &&
+ test_path_completion "\"spaces i" \
+ "spaces in dir" &&
+ test_path_completion "\"spaces in dir/s" \
+ "spaces in dir/spaces in file" &&
+ test_path_completion "\"spaces in dir/spaces i" \
+ "spaces in dir/spaces in file"
+'
+
+test_expect_success '__git_complete_index_file - UTF-8 in ls-files output' '
+ test_path_completion á árvíztűrő &&
+ test_path_completion árvíztűrő/С "árvíztűrő/Сайн яваарай"
+'
+
+test_expect_success FUNNYNAMES \
+ '__git_complete_index_file - C-style escapes in ls-files output' '
+ test_path_completion BS \
+ BS\\dir &&
+ test_path_completion BS\\\\d \
+ BS\\dir &&
+ test_path_completion BS\\\\dir/DQ \
+ BS\\dir/DQ\"file &&
+ test_path_completion BS\\\\dir/DQ\\\"f \
+ BS\\dir/DQ\"file
+'
+
+test_expect_success FUNNYNAMES \
+ '__git_complete_index_file - \nnn-escaped characters in ls-files output' '
+ test_path_completion sep '$'separators\034in\035dir'' &&
+ test_path_completion '$'separators\034i'' \
+ '$'separators\034in\035dir'' &&
+ test_path_completion '$'separators\034in\035dir/sep'' \
+ '$'separators\034in\035dir/sep\036in\037file'' &&
+ test_path_completion '$'separators\034in\035dir/sep\036i'' \
+ '$'separators\034in\035dir/sep\036in\037file''
+'
+
+test_expect_success FUNNYNAMES \
+ '__git_complete_index_file - removing repeated quoted path components' '
+ test_when_finished rm -r repeated-quoted &&
+ mkdir repeated-quoted && # A directory whose name in itself
+ # would not be quoted ...
+ >repeated-quoted/0-file &&
+ >repeated-quoted/1\"file && # ... but here the file makes the
+ # dirname quoted ...
+ >repeated-quoted/2-file &&
+ >repeated-quoted/3\"file && # ... and here, too.
+
+ # Still, we shold only list the directory name only once.
+ test_path_completion repeated repeated-quoted
+'
+
+test_expect_success 'teardown after path completion tests' '
+ rm -rf simple-dir "spaces in dir" árvíztűrő \
+ BS\\dir '$'separators\034in\035dir''
+'
+
+
test_expect_success '__git_get_config_variables' '
cat >expect <<-EOF &&
name-1
echo "expected" > .gitignore &&
echo "out" >> .gitignore &&
+ echo "out_sorted" >> .gitignore &&
git add .gitignore &&
test_completion "git commit " ".gitignore" &&
"$SHELL_PATH" <&6 >&5 2>&7
}
-# Wrap git in gdb. Adding this to a command can make it easier to
-# understand what is going on in a failing test.
+# Wrap git with a debugger. Adding this to a command can make it easier
+# to understand what is going on in a failing test.
#
-# Example: "debug git checkout master".
+# Examples:
+# debug git checkout master
+# debug --debugger=nemiver git $ARGS
+# debug -d "valgrind --tool=memcheck --track-origins=yes" git $ARGS
debug () {
- GIT_TEST_GDB=1 "$@" <&6 >&5 2>&7
+ case "$1" in
+ -d)
+ GIT_DEBUGGER="$2" &&
+ shift 2
+ ;;
+ --debugger=*)
+ GIT_DEBUGGER="${1#*=}" &&
+ shift 1
+ ;;
+ *)
+ GIT_DEBUGGER=1
+ ;;
+ esac &&
+ GIT_DEBUGGER="${GIT_DEBUGGER}" "$@" <&6 >&5 2>&7
}
# Call test_commit with the arguments
# The single parameter is the prerequisite tag (a simple word, in all
# capital letters by convention).
+test_unset_prereq () {
+ ! test_have_prereq "$1" ||
+ satisfied_prereq="${satisfied_prereq% $1 *} ${satisfied_prereq#* $1 }"
+}
+
test_set_prereq () {
- satisfied_prereq="$satisfied_prereq$1 "
+ case "$1" in
+ !*)
+ test_unset_prereq "${1#!}"
+ ;;
+ *)
+ satisfied_prereq="$satisfied_prereq$1 "
+ ;;
+ esac
}
satisfied_prereq=" "
lazily_testable_prereq= lazily_tested_prereq=
auml=$(printf "\303\244")
aumlcdiar=$(printf "\141\314\210")
>"$auml" &&
- case "$(echo *)" in
- "$aumlcdiar")
- true ;;
- *)
- false ;;
- esac
+ test -f "$aumlcdiar"
'
test_lazy_prereq AUTOIDENT '
unsigned long size;
int ret;
- type = oid_object_info(oid, NULL);
+ type = oid_object_info(the_repository, oid, NULL);
if (type != OBJ_TAG)
return error("%s: cannot verify a non-tag object of type %s.",
name_to_report ?
against=HEAD
else
# Initial commit: diff against an empty tree object
- against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
+ against=$(git hash-object -t tree /dev/null)
fi
# If you want to allow non-ASCII filenames set this variable to true.
struct tmp_objdir *t;
if (the_tmp_objdir)
- die("BUG: only one tmp_objdir can be used at a time");
+ BUG("only one tmp_objdir can be used at a time");
t = xmalloc(sizeof(*t));
strbuf_init(&t->path, 0);
free_arg_item(arg_tok);
break;
default:
- die("BUG: trailer.c: unhandled value %d",
+ BUG("trailer.c: unhandled value %d",
arg_tok->conf.if_exists);
}
}
list_add(&to_add->list, head);
break;
default:
- die("BUG: trailer.c: unhandled value %d",
+ BUG("trailer.c: unhandled value %d",
arg_tok->conf.if_missing);
}
}
warning(_("unknown value '%s' for key '%s'"), value, conf_key);
break;
default:
- die("BUG: trailer.c: unhandled type %d", type);
+ BUG("trailer.c: unhandled type %d", type);
}
return 0;
}
#include "sigchain.h"
#include "argv-array.h"
#include "refs.h"
+#include "refspec.h"
#include "transport-internal.h"
#include "protocol.h"
char *export_marks;
char *import_marks;
/* These go from remote name (as in "list") to private name */
- struct refspec *refspecs;
- int refspec_nr;
+ struct refspec rs;
/* Transport options for fetch-pack/send-pack (should one of
* those be invoked).
*/
struct helper_data *data = transport->data;
struct strbuf buf = STRBUF_INIT;
struct child_process *helper;
- const char **refspecs = NULL;
- int refspec_nr = 0;
- int refspec_alloc = 0;
int duped;
int code;
data->helper = helper;
data->no_disconnect_req = 0;
+ refspec_init(&data->rs, REFSPEC_FETCH);
/*
* Open the output as FILE* so strbuf_getline_*() family of
data->export = 1;
else if (!strcmp(capname, "check-connectivity"))
data->check_connectivity = 1;
- else if (!data->refspecs && skip_prefix(capname, "refspec ", &arg)) {
- ALLOC_GROW(refspecs,
- refspec_nr + 1,
- refspec_alloc);
- refspecs[refspec_nr++] = xstrdup(arg);
+ else if (skip_prefix(capname, "refspec ", &arg)) {
+ refspec_append(&data->rs, arg);
} else if (!strcmp(capname, "connect")) {
data->connect = 1;
} else if (!strcmp(capname, "stateless-connect")) {
capname);
}
}
- if (refspecs) {
- int i;
- data->refspec_nr = refspec_nr;
- data->refspecs = parse_fetch_refspec(refspec_nr, refspecs);
- for (i = 0; i < refspec_nr; i++)
- free((char *)refspecs[i]);
- free(refspecs);
- } else if (data->import || data->bidi_import || data->export) {
+ if (!data->rs.nr && (data->import || data->bidi_import || data->export)) {
warning("This remote helper should implement refspec capability.");
}
strbuf_release(&buf);
{
int res = 0;
struct helper_data *data = transport->data;
- free_refspec(data->refspec_nr, data->refspecs);
- data->refspecs = NULL;
+ refspec_clear(&data->rs);
res = disconnect_helper(transport);
free(transport->data);
return res;
if (posn->status & REF_STATUS_UPTODATE)
continue;
name = posn->symref ? posn->symref : posn->name;
- if (data->refspecs)
- private = apply_refspecs(data->refspecs, data->refspec_nr, name);
+ if (data->rs.nr)
+ private = apply_refspecs(&data->rs, name);
else
private = xstrdup(name);
if (private) {
if (push_update_ref_status(&buf, &ref, remote_refs))
continue;
- if (flags & TRANSPORT_PUSH_DRY_RUN || !data->refspecs || data->no_private_update)
+ if (flags & TRANSPORT_PUSH_DRY_RUN || !data->rs.nr || data->no_private_update)
continue;
/* propagate back the update to the remote namespace */
- private = apply_refspecs(data->refspecs, data->refspec_nr, ref->name);
+ private = apply_refspecs(&data->rs, ref->name);
if (!private)
continue;
update_ref("update by helper", private, &ref->new_oid, NULL,
struct string_list revlist_args = STRING_LIST_INIT_DUP;
struct strbuf buf = STRBUF_INIT;
- if (!data->refspecs)
+ if (!data->rs.nr)
die("remote-helper doesn't support push; refspec needed");
set_common_push_options(transport, data->name, flags);
char *private;
struct object_id oid;
- private = apply_refspecs(data->refspecs, data->refspec_nr, ref->name);
+ private = apply_refspecs(&data->rs, ref->name);
if (private && !get_oid(private, &oid)) {
strbuf_addf(&buf, "^%s", private);
string_list_append_nodup(&revlist_args,
#include "bundle.h"
#include "dir.h"
#include "refs.h"
+#include "refspec.h"
#include "branch.h"
#include "url.h"
#include "submodule.h"
switch (data->version) {
case protocol_v2:
get_remote_refs(data->fd[1], &reader, &refs, for_push,
- ref_prefixes);
+ ref_prefixes, transport->server_options);
break;
case protocol_v1:
case protocol_v0:
args.no_dependents = data->options.no_dependents;
args.filter_options = data->options.filter_options;
args.stateless_rpc = transport->stateless_rpc;
+ args.server_options = transport->server_options;
if (!data->got_remote_heads)
refs_tmp = get_refs_via_connect(transport, 0, NULL);
void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose)
{
- struct refspec rs;
+ struct refspec_item rs;
if (ref->status != REF_STATUS_OK && ref->status != REF_STATUS_UPTODATE)
return;
free(head);
}
-void transport_verify_remote_names(int nr_heads, const char **heads)
-{
- int i;
-
- for (i = 0; i < nr_heads; i++) {
- const char *local = heads[i];
- const char *remote = strrchr(heads[i], ':');
-
- if (*local == '+')
- local++;
-
- /* A matching refspec is okay. */
- if (remote == local && remote[1] == '\0')
- continue;
-
- remote = remote ? (remote + 1) : local;
- if (check_refname_format(remote,
- REFNAME_ALLOW_ONELEVEL|REFNAME_REFSPEC_PATTERN))
- die("remote part of refspec is not a valid name in %s",
- heads[i]);
- }
-}
-
static int git_transport_push(struct transport *transport, struct ref *remote_refs, int flags)
{
struct git_transport_data *data = transport->data;
struct git_transport_data *data;
if (!transport->smart_options)
- die("BUG: taking over transport requires non-NULL "
+ BUG("taking over transport requires non-NULL "
"smart_options field.");
data = xcalloc(1, sizeof(*data));
return from_user;
}
- die("BUG: invalid protocol_allow_config type");
+ BUG("invalid protocol_allow_config type");
}
void transport_check_allowed(const char *type)
}
int transport_push(struct transport *transport,
- int refspec_nr, const char **refspec, int flags,
+ struct refspec *rs, int flags,
unsigned int *reject_reasons)
{
*reject_reasons = 0;
- transport_verify_remote_names(refspec_nr, refspec);
if (transport_color_config() < 0)
return -1;
int porcelain = flags & TRANSPORT_PUSH_PORCELAIN;
int pretend = flags & TRANSPORT_PUSH_DRY_RUN;
int push_ret, ret, err;
- struct refspec *tmp_rs;
struct argv_array ref_prefixes = ARGV_ARRAY_INIT;
- int i;
- if (check_push_refs(local_refs, refspec_nr, refspec) < 0)
+ if (check_push_refs(local_refs, rs) < 0)
return -1;
- tmp_rs = parse_push_refspec(refspec_nr, refspec);
- for (i = 0; i < refspec_nr; i++) {
- const char *prefix = NULL;
-
- if (tmp_rs[i].dst)
- prefix = tmp_rs[i].dst;
- else if (tmp_rs[i].src && !tmp_rs[i].exact_sha1)
- prefix = tmp_rs[i].src;
-
- if (prefix) {
- const char *glob = strchr(prefix, '*');
- if (glob)
- argv_array_pushf(&ref_prefixes, "%.*s",
- (int)(glob - prefix),
- prefix);
- else
- expand_ref_prefix(&ref_prefixes, prefix);
- }
- }
+ refspec_ref_prefixes(rs, &ref_prefixes);
remote_refs = transport->vtable->get_refs_list(transport, 1,
&ref_prefixes);
argv_array_clear(&ref_prefixes);
- free_refspec(refspec_nr, tmp_rs);
if (flags & TRANSPORT_PUSH_ALL)
match_flags |= MATCH_REFS_ALL;
if (flags & TRANSPORT_PUSH_FOLLOW_TAGS)
match_flags |= MATCH_REFS_FOLLOW_TAGS;
- if (match_push_refs(local_refs, &remote_refs,
- refspec_nr, refspec, match_flags)) {
+ if (match_push_refs(local_refs, &remote_refs, rs, match_flags))
return -1;
- }
if (transport->smart_options &&
transport->smart_options->cas &&
if (!push_unpushed_submodules(&commits,
transport->remote,
- refspec, refspec_nr,
+ rs,
transport->push_options,
pretend)) {
oid_array_clear(&commits);
*/
const struct string_list *push_options;
+ /*
+ * These strings will be passed to the remote side on each command
+ * request, if both sides support the server-option capability.
+ */
+ const struct string_list *server_options;
+
char *pack_lockfile;
signed verbose : 3;
/**
#define REJECT_NEEDS_FORCE 0x10
int transport_push(struct transport *connection,
- int refspec_nr, const char **refspec, int flags,
+ struct refspec *rs, int flags,
unsigned int * reject_reasons);
/*
int bidirectional_transfer_loop(int input, int output);
/* common methods used by transport.c and builtin/send-pack.c */
-void transport_verify_remote_names(int nr_heads, const char **heads);
-
void transport_update_tracking_ref(struct remote *remote, struct ref *ref, int verbose);
int transport_refs_pushed(struct ref *ref);
static int update_tree_entry_internal(struct tree_desc *desc, struct strbuf *err)
{
const void *buf = desc->buffer;
- const unsigned char *end = desc->entry.oid->hash + 20;
+ const unsigned char *end = desc->entry.oid->hash + the_hash_algo->rawsz;
unsigned long size = desc->size;
unsigned long len = end - (const unsigned char *)buf;
struct dir_state {
void *tree;
unsigned long size;
- unsigned char sha1[20];
+ struct object_id oid;
};
static int find_tree_entry(struct tree_desc *t, const char *name, struct object_id *result, unsigned *mode)
* See the code for enum follow_symlink_result for a description of
* the return values.
*/
-enum follow_symlinks_result get_tree_entry_follow_symlinks(unsigned char *tree_sha1, const char *name, unsigned char *result, struct strbuf *result_path, unsigned *mode)
+enum follow_symlinks_result get_tree_entry_follow_symlinks(struct object_id *tree_oid, const char *name, struct object_id *result, struct strbuf *result_path, unsigned *mode)
{
int retval = MISSING_OBJECT;
struct dir_state *parents = NULL;
init_tree_desc(&t, NULL, 0UL);
strbuf_addstr(&namebuf, name);
- hashcpy(current_tree_oid.hash, tree_sha1);
+ oidcpy(¤t_tree_oid, tree_oid);
while (1) {
int find_result;
ALLOC_GROW(parents, parents_nr + 1, parents_alloc);
parents[parents_nr].tree = tree;
parents[parents_nr].size = size;
- hashcpy(parents[parents_nr].sha1, root.hash);
+ oidcpy(&parents[parents_nr].oid, &root);
parents_nr++;
if (namebuf.buf[0] == '\0') {
- hashcpy(result, root.hash);
+ oidcpy(result, &root);
retval = FOUND;
goto done;
}
/* We could end up here via a symlink to dir/.. */
if (namebuf.buf[0] == '\0') {
- hashcpy(result, parents[parents_nr - 1].sha1);
+ oidcpy(result, &parents[parents_nr - 1].oid);
retval = FOUND;
goto done;
}
if (S_ISDIR(*mode)) {
if (!remainder) {
- hashcpy(result, current_tree_oid.hash);
+ oidcpy(result, ¤t_tree_oid);
retval = FOUND;
goto done;
}
1 + first_slash - namebuf.buf);
} else if (S_ISREG(*mode)) {
if (!remainder) {
- hashcpy(result, current_tree_oid.hash);
+ oidcpy(result, ¤t_tree_oid);
retval = FOUND;
} else {
retval = NOT_DIR;
*/
};
-enum follow_symlinks_result get_tree_entry_follow_symlinks(unsigned char *tree_sha1, const char *name, unsigned char *result, struct strbuf *result_path, unsigned *mode);
+enum follow_symlinks_result get_tree_entry_follow_symlinks(struct object_id *tree_oid, const char *name, struct object_id *result, struct strbuf *result_path, unsigned *mode);
struct traverse_info {
const char *traverse_path;
oid_to_hex(entry.oid),
base->buf, entry.path);
- oidcpy(&oid, &commit->tree->object.oid);
+ oidcpy(&oid, get_commit_tree_oid(commit));
}
else
continue;
if (obj->type == OBJ_TREE)
return (struct tree *) obj;
else if (obj->type == OBJ_COMMIT)
- obj = &(((struct commit *) obj)->tree->object);
+ obj = &(get_commit_tree(((struct commit *)obj))->object);
else if (obj->type == OBJ_TAG)
obj = ((struct tag *) obj)->tagged;
else
if (ce->ce_flags & CE_UPDATE) {
if (ce->ce_flags & CE_WT_REMOVE)
- die("BUG: both update and delete flags are set on %s",
+ BUG("both update and delete flags are set on %s",
ce->name);
display_progress(progress, ++cnt);
ce->ce_flags &= ~CE_UPDATE;
o->result.timestamp.sec = o->src_index->timestamp.sec;
o->result.timestamp.nsec = o->src_index->timestamp.nsec;
o->result.version = o->src_index->version;
- o->result.split_index = o->src_index->split_index;
- if (o->result.split_index)
+ if (!o->src_index->split_index) {
+ o->result.split_index = NULL;
+ } else if (o->src_index == o->dst_index) {
+ /*
+ * o->dst_index (and thus o->src_index) will be discarded
+ * and overwritten with o->result at the end of this function,
+ * so just use src_index's split_index to avoid having to
+ * create a new one.
+ */
+ o->result.split_index = o->src_index->split_index;
o->result.split_index->refcount++;
- hashcpy(o->result.sha1, o->src_index->sha1);
+ } else {
+ o->result.split_index = init_split_index(&o->result);
+ }
+ oidcpy(&o->result.oid, &o->src_index->oid);
o->merge_size = len;
mark_all_ce_unused(o->src_index);
}
}
- o->src_index = NULL;
ret = check_updates(o) ? (-2) : 0;
if (o->dst_index) {
if (!ret) {
WRITE_TREE_SILENT |
WRITE_TREE_REPAIR);
}
- move_index_extensions(&o->result, o->dst_index);
+ move_index_extensions(&o->result, o->src_index);
discard_index(o->dst_index);
*o->dst_index = o->result;
} else {
discard_index(&o->result);
}
+ o->src_index = NULL;
done:
clear_exclude_list(&el);
add_rejected_path(o, error_type, ce->name);
}
-static int verify_uptodate(const struct cache_entry *ce,
- struct unpack_trees_options *o)
+int verify_uptodate(const struct cache_entry *ce,
+ struct unpack_trees_options *o)
{
if (!o->skip_sparse_checkout && (ce->ce_flags & CE_NEW_SKIP_WORKTREE))
return 0;
#ifndef UNPACK_TREES_H
#define UNPACK_TREES_H
+#include "tree-walk.h"
#include "string-list.h"
#define MAX_UNPACK_TREES 8
extern int unpack_trees(unsigned n, struct tree_desc *t,
struct unpack_trees_options *options);
+int verify_uptodate(const struct cache_entry *ce,
+ struct unpack_trees_options *o);
+
int threeway_merge(const struct cache_entry * const *stages,
struct unpack_trees_options *o);
int twoway_merge(const struct cache_entry * const *src,
break;
default:
got_common = 1;
- memcpy(last_hex, oid_to_hex(&oid), 41);
+ oid_to_hex_r(last_hex, &oid);
if (multi_ack == 2)
packet_write_fmt(1, "ACK %s common\n", last_hex);
else if (multi_ack)
"rev-list", "--stdin", NULL,
};
struct object *o;
- char namebuf[42]; /* ^ + SHA-1 + LF */
+ char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */
int i;
cmd->argv = argv;
struct child_process cmd = CHILD_PROCESS_INIT;
int i;
struct object *o;
- char namebuf[42]; /* ^ + SHA-1 + LF */
+ char namebuf[GIT_MAX_HEXSZ + 2]; /* ^ + hash + LF */
+ const unsigned hexsz = the_hash_algo->hexsz;
if (do_reachable_revlist(&cmd, src, reachable) < 0)
return -1;
- while ((i = read_in_full(cmd.out, namebuf, 41)) == 41) {
+ while ((i = read_in_full(cmd.out, namebuf, hexsz + 1)) == hexsz + 1) {
struct object_id sha1;
+ const char *p;
- if (namebuf[40] != '\n' || get_oid_hex(namebuf, &sha1))
+ if (parse_oid_hex(namebuf, &sha1, &p) || *p != '\n')
break;
o = lookup_object(sha1.hash);
}
if (!skip_prefix(line, "want ", &arg) ||
- get_oid_hex(arg, &oid_buf))
+ parse_oid_hex(arg, &oid_buf, &features))
die("git upload-pack: protocol error, "
- "expected to get sha, not '%s'", line);
-
- features = arg + 40;
+ "expected to get object ID, not '%s'", line);
if (parse_feature_request(features, "deepen-relative"))
deepen_relative = 1;
{
while (packet_reader_read(request) != PACKET_READ_FLUSH) {
const char *arg = request->line;
+ const char *p;
/* process want */
if (parse_want(arg))
continue;
}
+ if (allow_filter && skip_prefix(arg, "filter ", &p)) {
+ parse_list_objects_filter(&filter_options, p);
+ continue;
+ }
+
/* ignore unknown lines maybe? */
- die("unexpect line: '%s'", arg);
+ die("unexpected line: '%s'", arg);
}
}
enum fetch_state state = FETCH_PROCESS_ARGS;
struct upload_pack_data data;
+ git_config(upload_pack_config, NULL);
+
upload_pack_data_init(&data);
use_sideband = LARGE_PACKET_MAX;
int upload_pack_advertise(struct repository *r,
struct strbuf *value)
{
- if (value)
+ if (value) {
+ int allow_filter_value;
strbuf_addstr(value, "shallow");
+ if (!repo_config_get_bool(the_repository,
+ "uploadpack.allowfilter",
+ &allow_filter_value) &&
+ allow_filter_value)
+ strbuf_addstr(value, " filter");
+ }
return 1;
}
va_end(params);
}
+/* Only set this, ever, from t/helper/, when verifying that bugs are caught. */
+int BUG_exit_code;
+
static NORETURN void BUG_vfl(const char *file, int line, const char *fmt, va_list params)
{
char prefix[256];
snprintf(prefix, sizeof(prefix), "BUG: ");
vreportf(prefix, fmt, params);
+ if (BUG_exit_code)
+ exit(BUG_exit_code);
abort();
}
}
}
-int is_hfs_dotgit(const char *path)
+static int is_hfs_dot_generic(const char *path,
+ const char *needle, size_t needle_len)
{
ucs_char_t c;
c = next_hfs_char(&path);
if (c != '.')
return 0;
- c = next_hfs_char(&path);
/*
* there's a great deal of other case-folding that occurs
- * in HFS+, but this is enough to catch anything that will
- * convert to ".git"
+ * in HFS+, but this is enough to catch our fairly vanilla
+ * hard-coded needles.
*/
- if (c != 'g' && c != 'G')
- return 0;
- c = next_hfs_char(&path);
- if (c != 'i' && c != 'I')
- return 0;
- c = next_hfs_char(&path);
- if (c != 't' && c != 'T')
- return 0;
+ for (; needle_len > 0; needle++, needle_len--) {
+ c = next_hfs_char(&path);
+
+ /*
+ * We know our needles contain only ASCII, so we clamp here to
+ * make the results of tolower() sane.
+ */
+ if (c > 127)
+ return 0;
+ if (tolower(c) != *needle)
+ return 0;
+ }
+
c = next_hfs_char(&path);
if (c && !is_dir_sep(c))
return 0;
return 1;
}
+/*
+ * Inline wrapper to make sure the compiler resolves strlen() on literals at
+ * compile time.
+ */
+static inline int is_hfs_dot_str(const char *path, const char *needle)
+{
+ return is_hfs_dot_generic(path, needle, strlen(needle));
+}
+
+int is_hfs_dotgit(const char *path)
+{
+ return is_hfs_dot_str(path, "git");
+}
+
+int is_hfs_dotgitmodules(const char *path)
+{
+ return is_hfs_dot_str(path, "gitmodules");
+}
+
+int is_hfs_dotgitignore(const char *path)
+{
+ return is_hfs_dot_str(path, "gitignore");
+}
+
+int is_hfs_dotgitattributes(const char *path)
+{
+ return is_hfs_dot_str(path, "gitattributes");
+}
+
const char utf8_bom[] = "\357\273\277";
int skip_utf8_bom(char **text, size_t len)
* The path should be NUL-terminated, but we will match variants of both ".git\0"
* and ".git/..." (but _not_ ".../.git"). This makes it suitable for both fsck
* and verify_path().
+ *
+ * Likewise, the is_hfs_dotgitfoo() variants look for ".gitfoo".
*/
int is_hfs_dotgit(const char *path);
+int is_hfs_dotgitmodules(const char *path);
+int is_hfs_dotgitignore(const char *path);
+int is_hfs_dotgitattributes(const char *path);
typedef enum {
ALIGN_LEFT,
err = fast_export_ls(path, mode_out, &buf);
if (err) {
if (errno != ENOENT)
- die_errno("BUG: unexpected fast_export_ls error");
+ BUG("unexpected fast_export_ls error: %s",
+ strerror(errno));
/* Treat missing paths as directories. */
*mode_out = S_IFDIR;
return NULL;
err = fast_export_ls_rev(revision, src, &mode, &data);
if (err) {
if (errno != ENOENT)
- die_errno("BUG: unexpected fast_export_ls_rev error");
+ BUG("unexpected fast_export_ls_rev error: %s",
+ strerror(errno));
fast_export_delete(dst);
return;
}
walker_say(walker, "walk %s\n", oid_to_hex(&commit->object.oid));
- if (process(walker, &commit->tree->object))
+ if (process(walker, &get_commit_tree(commit)->object))
return -1;
for (parents = commit->parents; parents; parents = parents->next) {
struct strbuf path = STRBUF_INIT;
if (is_main_worktree(wt))
- die("BUG: can't relocate main worktree");
+ BUG("can't relocate main worktree");
strbuf_realpath(&path, path_, 1);
if (fspathcmp(wt->path, path.buf)) {
export GIT_EXEC_PATH GITPERLLIB PATH GIT_TEXTDOMAINDIR
-if test -n "$GIT_TEST_GDB"
-then
- unset GIT_TEST_GDB
- exec gdb --args "${GIT_EXEC_PATH}/@@PROG@@" "$@"
-else
+case "$GIT_DEBUGGER" in
+'')
exec "${GIT_EXEC_PATH}/@@PROG@@" "$@"
-fi
+ ;;
+1)
+ unset GIT_DEBUGGER
+ exec gdb --args "${GIT_EXEC_PATH}/@@PROG@@" "$@"
+ ;;
+*)
+ GIT_DEBUGGER_ARGS="$GIT_DEBUGGER"
+ unset GIT_DEBUGGER
+ exec ${GIT_DEBUGGER_ARGS} "${GIT_EXEC_PATH}/@@PROG@@" "$@"
+ ;;
+esac
va_end(ap);
if (len < 0)
- die("BUG: your snprintf is broken");
+ BUG("your snprintf is broken");
if (len >= max)
- die("BUG: attempt to snprintf into too-small buffer");
+ BUG("attempt to snprintf into too-small buffer");
return len;
}
s->show_stash = 0;
s->ahead_behind_flags = AHEAD_BEHIND_UNSPECIFIED;
s->display_comment_prefix = 0;
+ s->detect_rename = -1;
+ s->rename_score = -1;
+ s->rename_limit = -1;
}
static void wt_longstatus_print_unmerged_header(struct wt_status *s)
case 7:
return _("both modified:");
default:
- die("BUG: unhandled unmerged status %x", stagemask);
+ BUG("unhandled unmerged status %x", stagemask);
}
}
status = d->worktree_status;
break;
default:
- die("BUG: unhandled change_type %d in wt_longstatus_print_change_data",
+ BUG("unhandled change_type %d in wt_longstatus_print_change_data",
change_type);
}
status_printf(s, color(WT_STATUS_HEADER, s), "\t");
what = wt_status_diff_status_string(status);
if (!what)
- die("BUG: unhandled diff status %c", status);
+ BUG("unhandled diff status %c", status);
len = label_width - utf8_strwidth(what);
assert(len >= 0);
if (one_name != two_name)
case DIFF_STATUS_COPIED:
case DIFF_STATUS_RENAMED:
if (d->rename_status)
- die("BUG: multiple renames on the same target? how?");
+ BUG("multiple renames on the same target? how?");
d->rename_source = xstrdup(p->one->path);
d->rename_score = p->score * 100 / MAX_SCORE;
d->rename_status = p->status;
break;
default:
- die("BUG: unhandled diff-files status '%c'", p->status);
+ BUG("unhandled diff-files status '%c'", p->status);
break;
}
case DIFF_STATUS_COPIED:
case DIFF_STATUS_RENAMED:
if (d->rename_status)
- die("BUG: multiple renames on the same target? how?");
+ BUG("multiple renames on the same target? how?");
d->rename_source = xstrdup(p->one->path);
d->rename_score = p->score * 100 / MAX_SCORE;
d->rename_status = p->status;
break;
default:
- die("BUG: unhandled diff-index status '%c'", p->status);
+ BUG("unhandled diff-index status '%c'", p->status);
break;
}
}
}
rev.diffopt.format_callback = wt_status_collect_changed_cb;
rev.diffopt.format_callback_data = s;
+ rev.diffopt.detect_rename = s->detect_rename >= 0 ? s->detect_rename : rev.diffopt.detect_rename;
+ rev.diffopt.rename_limit = s->rename_limit >= 0 ? s->rename_limit : rev.diffopt.rename_limit;
+ rev.diffopt.rename_score = s->rename_score >= 0 ? s->rename_score : rev.diffopt.rename_score;
copy_pathspec(&rev.prune_data, &s->pathspec);
run_diff_files(&rev, 0);
}
init_revisions(&rev, NULL);
memset(&opt, 0, sizeof(opt));
- opt.def = s->is_initial ? EMPTY_TREE_SHA1_HEX : s->reference;
+ opt.def = s->is_initial ? empty_tree_oid_hex() : s->reference;
setup_revisions(0, NULL, &rev, &opt);
rev.diffopt.flags.override_submodule_config = 1;
rev.diffopt.output_format |= DIFF_FORMAT_CALLBACK;
rev.diffopt.format_callback = wt_status_collect_updated_cb;
rev.diffopt.format_callback_data = s;
- rev.diffopt.detect_rename = DIFF_DETECT_RENAME;
- rev.diffopt.rename_limit = 200;
- rev.diffopt.break_opt = 0;
+ rev.diffopt.detect_rename = s->detect_rename >= 0 ? s->detect_rename : rev.diffopt.detect_rename;
+ rev.diffopt.rename_limit = s->rename_limit >= 0 ? s->rename_limit : rev.diffopt.rename_limit;
+ rev.diffopt.rename_score = s->rename_score >= 0 ? s->rename_score : rev.diffopt.rename_score;
copy_pathspec(&rev.prune_data, &s->pathspec);
run_diff_index(&rev, 1);
}
rev.diffopt.ita_invisible_in_index = 1;
memset(&opt, 0, sizeof(opt));
- opt.def = s->is_initial ? EMPTY_TREE_SHA1_HEX : s->reference;
+ opt.def = s->is_initial ? empty_tree_oid_hex() : s->reference;
setup_revisions(0, NULL, &rev, &opt);
rev.diffopt.output_format |= DIFF_FORMAT_PATCH;
- rev.diffopt.detect_rename = DIFF_DETECT_RENAME;
+ rev.diffopt.detect_rename = s->detect_rename >= 0 ? s->detect_rename : rev.diffopt.detect_rename;
+ rev.diffopt.rename_limit = s->rename_limit >= 0 ? s->rename_limit : rev.diffopt.rename_limit;
+ rev.diffopt.rename_score = s->rename_score >= 0 ? s->rename_score : rev.diffopt.rename_score;
rev.diffopt.file = s->fp;
rev.diffopt.close_file = 0;
/*
case 6: key = "AA"; break; /* both added */
case 7: key = "UU"; break; /* both modified */
default:
- die("BUG: unhandled unmerged status %x", d->stagemask);
+ BUG("unhandled unmerged status %x", d->stagemask);
}
/*
sum |= (1 << (stage - 1));
}
if (sum != d->stagemask)
- die("BUG: observed stagemask 0x%x != expected stagemask 0x%x", sum, d->stagemask);
+ BUG("observed stagemask 0x%x != expected stagemask 0x%x", sum, d->stagemask);
if (s->null_termination)
path_index = it->string;
wt_porcelain_v2_print(s);
break;
case STATUS_FORMAT_UNSPECIFIED:
- die("BUG: finalize_deferred_config() should have been called");
+ BUG("finalize_deferred_config() should have been called");
break;
case STATUS_FORMAT_NONE:
case STATUS_FORMAT_LONG:
int show_stash;
int hints;
enum ahead_behind_flags ahead_behind_flags;
-
+ int detect_rename;
+ int rename_score;
+ int rename_limit;
enum wt_status_format status_format;
unsigned char sha1_commit[GIT_MAX_RAWSZ]; /* when not Initial */
bytes_consumed = s->z.next_in - s->next_in;
bytes_produced = s->z.next_out - s->next_out;
if (s->z.total_out != s->total_out + bytes_produced)
- die("BUG: total_out mismatch");
+ BUG("total_out mismatch");
if (s->z.total_in != s->total_in + bytes_consumed)
- die("BUG: total_in mismatch");
+ BUG("total_in mismatch");
s->total_out = s->z.total_out;
s->total_in = s->z.total_in;