diff options
author | Junio C Hamano <junkio@cox.net> | 2007-02-06 19:33:22 -0800 |
---|---|---|
committer | Junio C Hamano <junkio@cox.net> | 2007-02-06 19:33:22 -0800 |
commit | 41dc7e004491725e74e3fdd1c364b4c0a234574b (patch) | |
tree | d4a177ce56cfb96e15151e26fbec37c2ea2400de | |
parent | Remove contrib/colordiff (diff) | |
parent | S_IFLNK != 0140000 (diff) | |
download | tgif-41dc7e004491725e74e3fdd1c364b4c0a234574b.tar.xz |
Merge branch 'master' of git://repo.or.cz/git/fastimport
* 'master' of git://repo.or.cz/git/fastimport: (81 commits)
S_IFLNK != 0140000
Don't do non-fastforward updates in fast-import.
Support RFC 2822 date parsing in fast-import.
Minor fast-import documentation corrections.
Remove unnecessary null pointer checks in fast-import.
Correct fast-import timezone documentation.
Correct minor style issue in fast-import.
Correct compiler warnings in fast-import.
Remove --branch-log from fast-import.
Initial draft of fast-import documentation.
Don't support shell-quoted refnames in fast-import.
Reduce memory usage of fast-import.
Include checkpoint command in the BNF.
Accept 'inline' file data in fast-import commit structure.
Reduce value duplication in t9300-fast-import.
Create test case for fast-import.
Support delimited data regions in fast-import.
Remove unnecessary options from fast-import.
Use fixed-size integers when writing out the index in fast-import.
Always use struct pack_header for pack header in fast-import.
...
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Documentation/git-fast-import.txt | 739 | ||||
-rw-r--r-- | Makefile | 1 | ||||
-rw-r--r-- | fast-import.c | 2039 | ||||
-rwxr-xr-x | t/t9300-fast-import.sh | 359 |
5 files changed, 3139 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore index a43444fb92..d49994903e 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ git-diff-index git-diff-stages git-diff-tree git-describe +git-fast-import git-fetch git-fetch-pack git-findtags diff --git a/Documentation/git-fast-import.txt b/Documentation/git-fast-import.txt new file mode 100644 index 0000000000..1fe2c1dcf2 --- /dev/null +++ b/Documentation/git-fast-import.txt @@ -0,0 +1,739 @@ +git-fast-import(1) +================== + +NAME +---- +git-fast-import - Backend for fast Git data importers. + + +SYNOPSIS +-------- +frontend | 'git-fast-import' [options] + +DESCRIPTION +----------- +This program is usually not what the end user wants to run directly. +Most end users want to use one of the existing frontend programs, +which parses a specific type of foreign source and feeds the contents +stored there to git-fast-import (gfi). + +gfi reads a mixed command/data stream from standard input and +writes one or more packfiles directly into the current repository. +When EOF is received on standard input, fast import writes out +updated branch and tag refs, fully updating the current repository +with the newly imported data. + +The gfi backend itself can import into an empty repository (one that +has already been initialized by gitlink:git-init[1]) or incrementally +update an existing populated repository. Whether or not incremental +imports are supported from a particular foreign source depends on +the frontend program in use. + + +OPTIONS +------- +--date-format=<fmt>:: + Specify the type of dates the frontend will supply to + gfi within `author`, `committer` and `tagger` commands. + See ``Date Formats'' below for details about which formats + are supported, and their syntax. + +--force:: + Force updating modified existing branches, even if doing + so would cause commits to be lost (as the new commit does + not contain the old commit). + +--max-pack-size=<n>:: + Maximum size of each output packfile, expressed in MiB. + The default is 4096 (4 GiB) as that is the maximum allowed + packfile size (due to file format limitations). Some + importers may wish to lower this, such as to ensure the + resulting packfiles fit on CDs. + +--depth=<n>:: + Maximum delta depth, for blob and tree deltification. + Default is 10. + +--active-branches=<n>:: + Maximum number of branches to maintain active at once. + See ``Memory Utilization'' below for details. Default is 5. + +--export-marks=<file>:: + Dumps the internal marks table to <file> when complete. + Marks are written one per line as `:markid SHA-1`. + Frontends can use this file to validate imports after they + have been completed. + +Performance +----------- +The design of gfi allows it to import large projects in a minimum +amount of memory usage and processing time. Assuming the frontend +is able to keep up with gfi and feed it a constant stream of data, +import times for projects holding 10+ years of history and containing +100,000+ individual commits are generally completed in just 1-2 +hours on quite modest (~$2,000 USD) hardware. + +Most bottlenecks appear to be in foreign source data access (the +source just cannot extract revisions fast enough) or disk IO (gfi +writes as fast as the disk will take the data). Imports will run +faster if the source data is stored on a different drive than the +destination Git repository (due to less IO contention). + + +Development Cost +---------------- +A typical frontend for gfi tends to weigh in at approximately 200 +lines of Perl/Python/Ruby code. Most developers have been able to +create working importers in just a couple of hours, even though it +is their first exposure to gfi, and sometimes even to Git. This is +an ideal situation, given that most conversion tools are throw-away +(use once, and never look back). + + +Parallel Operation +------------------ +Like `git-push` or `git-fetch`, imports handled by gfi are safe to +run alongside parallel `git repack -a -d` or `git gc` invocations, +or any other Git operation (including `git prune`, as loose objects +are never used by gfi). + +gfi does not lock the branch or tag refs it is actively importing. +After the import, during its ref update phase, gfi tests each +existing branch ref to verify the update will be a fast-forward +update (the commit stored in the ref is contained in the new +history of the commit to be written). If the update is not a +fast-forward update, gfi will skip updating that ref and instead +prints a warning message. gfi will always attempt to update all +branch refs, and does not stop on the first failure. + +Branch updates can be forced with `--force`, but its recommended that +this only be used on an otherwise quiet repository. Using `--force` +is not necessary for an initial import into an empty repository. + + +Technical Discussion +-------------------- +gfi tracks a set of branches in memory. Any branch can be created +or modified at any point during the import process by sending a +`commit` command on the input stream. This design allows a frontend +program to process an unlimited number of branches simultaneously, +generating commits in the order they are available from the source +data. It also simplifies the frontend programs considerably. + +gfi does not use or alter the current working directory, or any +file within it. (It does however update the current Git repository, +as referenced by `GIT_DIR`.) Therefore an import frontend may use +the working directory for its own purposes, such as extracting file +revisions from the foreign source. This ignorance of the working +directory also allows gfi to run very quickly, as it does not +need to perform any costly file update operations when switching +between branches. + +Input Format +------------ +With the exception of raw file data (which Git does not interpret) +the gfi input format is text (ASCII) based. This text based +format simplifies development and debugging of frontend programs, +especially when a higher level language such as Perl, Python or +Ruby is being used. + +gfi is very strict about its input. Where we say SP below we mean +*exactly* one space. Likewise LF means one (and only one) linefeed. +Supplying additional whitespace characters will cause unexpected +results, such as branch names or file names with leading or trailing +spaces in their name, or early termination of gfi when it encounters +unexpected input. + +Date Formats +~~~~~~~~~~~~ +The following date formats are supported. A frontend should select +the format it will use for this import by passing the format name +in the `--date-format=<fmt>` command line option. + +`raw`:: + This is the Git native format and is `<time> SP <tz>`. + It is also gfi's default format, if `--date-format` was + not specified. ++ +The time of the event is specified by `<time>` as the number of +seconds since the UNIX epoch (midnight, Jan 1, 1970, UTC) and is +written as an ASCII decimal integer. ++ +The timezone is specified by `<tz>` as a positive or negative offset +from UTC. For example EST (which is typically 5 hours behind GMT) +would be expressed in `<tz>` by ``-0500'' while GMT is ``+0000''. ++ +If the timezone is not available in the source material, use +``+0000'', or the most common local timezone. For example many +organizations have a CVS repository which has only ever been accessed +by users who are located in the same location and timezone. In this +case the user's timezone can be easily assumed. ++ +Unlike the `rfc2822` format, this format is very strict. Any +variation in formatting will cause gfi to reject the value. + +`rfc2822`:: + This is the standard email format as described by RFC 2822. ++ +An example value is ``Tue Feb 6 11:22:18 2007 -0500''. The Git +parser is accurate, but a little on the lenient side. Its the +same parser used by gitlink:git-am[1] when applying patches +received from email. ++ +Some malformed strings may be accepted as valid dates. In some of +these cases Git will still be able to obtain the correct date from +the malformed string. There are also some types of malformed +strings which Git will parse wrong, and yet consider valid. +Seriously malformed strings will be rejected. ++ +If the source material is formatted in RFC 2822 style dates, +the frontend should let gfi handle the parsing and conversion +(rather than attempting to do it itself) as the Git parser has +been well tested in the wild. ++ +Frontends should prefer the `raw` format if the source material +is already in UNIX-epoch format, or is easily convertible to +that format, as there is no ambiguity in parsing. + +`now`:: + Always use the current time and timezone. The literal + `now` must always be supplied for `<when>`. ++ +This is a toy format. The current time and timezone of this system +is always copied into the identity string at the time it is being +created by gfi. There is no way to specify a different time or +timezone. ++ +This particular format is supplied as its short to implement and +may be useful to a process that wants to create a new commit +right now, without needing to use a working directory or +gitlink:git-update-index[1]. ++ +If separate `author` and `committer` commands are used in a `commit` +the timestamps may not match, as the system clock will be polled +twice (once for each command). The only way to ensure that both +author and committer identity information has the same timestamp +is to omit `author` (thus copying from `committer`) or to use a +date format other than `now`. + +Commands +~~~~~~~~ +gfi accepts several commands to update the current repository +and control the current import process. More detailed discussion +(with examples) of each command follows later. + +`commit`:: + Creates a new branch or updates an existing branch by + creating a new commit and updating the branch to point at + the newly created commit. + +`tag`:: + Creates an annotated tag object from an existing commit or + branch. Lightweight tags are not supported by this command, + as they are not recommended for recording meaningful points + in time. + +`reset`:: + Reset an existing branch (or a new branch) to a specific + revision. This command must be used to change a branch to + a specific revision without making a commit on it. + +`blob`:: + Convert raw file data into a blob, for future use in a + `commit` command. This command is optional and is not + needed to perform an import. + +`checkpoint`:: + Forces gfi to close the current packfile, generate its + unique SHA-1 checksum and index, and start a new packfile. + This command is optional and is not needed to perform + an import. + +`commit` +~~~~~~~~ +Create or update a branch with a new commit, recording one logical +change to the project. + +.... + 'commit' SP <ref> LF + mark? + ('author' SP <name> SP LT <email> GT SP <when> LF)? + 'committer' SP <name> SP LT <email> GT SP <when> LF + data + ('from' SP <committish> LF)? + ('merge' SP <committish> LF)? + (filemodify | filedelete)* + LF +.... + +where `<ref>` is the name of the branch to make the commit on. +Typically branch names are prefixed with `refs/heads/` in +Git, so importing the CVS branch symbol `RELENG-1_0` would use +`refs/heads/RELENG-1_0` for the value of `<ref>`. The value of +`<ref>` must be a valid refname in Git. As `LF` is not valid in +a Git refname, no quoting or escaping syntax is supported here. + +A `mark` command may optionally appear, requesting gfi to save a +reference to the newly created commit for future use by the frontend +(see below for format). It is very common for frontends to mark +every commit they create, thereby allowing future branch creation +from any imported commit. + +The `data` command following `committer` must supply the commit +message (see below for `data` command syntax). To import an empty +commit message use a 0 length data. Commit messages are free-form +and are not interpreted by Git. Currently they must be encoded in +UTF-8, as gfi does not permit other encodings to be specified. + +Zero or more `filemodify` and `filedelete` commands may be +included to update the contents of the branch prior to the commit. +These commands can be supplied in any order, gfi is not sensitive +to pathname or operation ordering. + +`author` +^^^^^^^^ +An `author` command may optionally appear, if the author information +might differ from the committer information. If `author` is omitted +then gfi will automatically use the committer's information for +the author portion of the commit. See below for a description of +the fields in `author`, as they are identical to `committer`. + +`committer` +^^^^^^^^^^^ +The `committer` command indicates who made this commit, and when +they made it. + +Here `<name>` is the person's display name (for example +``Com M Itter'') and `<email>` is the person's email address +(``cm@example.com''). `LT` and `GT` are the literal less-than (\x3c) +and greater-than (\x3e) symbols. These are required to delimit +the email address from the other fields in the line. Note that +`<name>` is free-form and may contain any sequence of bytes, except +`LT` and `LF`. It is typically UTF-8 encoded. + +The time of the change is specified by `<when>` using the date format +that was selected by the `--date-format=<fmt>` command line option. +See ``Date Formats'' above for the set of supported formats, and +their syntax. + +`from` +^^^^^^ +Only valid for the first commit made on this branch by this +gfi process. The `from` command is used to specify the commit +to initialize this branch from. This revision will be the first +ancestor of the new commit. + +Omitting the `from` command in the first commit of a new branch will +cause gfi to create that commit with no ancestor. This tends to be +desired only for the initial commit of a project. Omitting the +`from` command on existing branches is required, as the current +commit on that branch is automatically assumed to be the first +ancestor of the new commit. + +As `LF` is not valid in a Git refname or SHA-1 expression, no +quoting or escaping syntax is supported within `<committish>`. + +Here `<committish>` is any of the following: + +* The name of an existing branch already in gfi's internal branch + table. If gfi doesn't know the name, its treated as a SHA-1 + expression. + +* A mark reference, `:<idnum>`, where `<idnum>` is the mark number. ++ +The reason gfi uses `:` to denote a mark reference is this character +is not legal in a Git branch name. The leading `:` makes it easy +to distingush between the mark 42 (`:42`) and the branch 42 (`42` +or `refs/heads/42`), or an abbreviated SHA-1 which happened to +consist only of base-10 digits. ++ +Marks must be declared (via `mark`) before they can be used. + +* A complete 40 byte or abbreviated commit SHA-1 in hex. + +* Any valid Git SHA-1 expression that resolves to a commit. See + ``SPECIFYING REVISIONS'' in gitlink:git-rev-parse[1] for details. + +The special case of restarting an incremental import from the +current branch value should be written as: +---- + from refs/heads/branch^0 +---- +The `^0` suffix is necessary as gfi does not permit a branch to +start from itself, and the branch is created in memory before the +`from` command is even read from the input. Adding `^0` will force +gfi to resolve the commit through Git's revision parsing library, +rather than its internal branch table, thereby loading in the +existing value of the branch. + +`merge` +^^^^^^^ +Includes one additional ancestor commit, and makes the current +commit a merge commit. An unlimited number of `merge` commands per +commit are permitted by gfi, thereby establishing an n-way merge. +However Git's other tools never create commits with more than 15 +additional ancestors (forming a 16-way merge). For this reason +it is suggested that frontends do not use more than 15 `merge` +commands per commit. + +Here `<committish>` is any of the commit specification expressions +also accepted by `from` (see above). + +`filemodify` +^^^^^^^^^^^^ +Included in a `commit` command to add a new file or change the +content of an existing file. This command has two different means +of specifying the content of the file. + +External data format:: + The data content for the file was already supplied by a prior + `blob` command. The frontend just needs to connect it. ++ +.... + 'M' SP <mode> SP <dataref> SP <path> LF +.... ++ +Here `<dataref>` can be either a mark reference (`:<idnum>`) +set by a prior `blob` command, or a full 40-byte SHA-1 of an +existing Git blob object. + +Inline data format:: + The data content for the file has not been supplied yet. + The frontend wants to supply it as part of this modify + command. ++ +.... + 'M' SP <mode> SP 'inline' SP <path> LF + data +.... ++ +See below for a detailed description of the `data` command. + +In both formats `<mode>` is the type of file entry, specified +in octal. Git only supports the following modes: + +* `100644` or `644`: A normal (not-executable) file. The majority + of files in most projects use this mode. If in doubt, this is + what you want. +* `100755` or `755`: A normal, but executable, file. +* `120000`: A symlink, the content of the file will be the link target. + +In both formats `<path>` is the complete path of the file to be added +(if not already existing) or modified (if already existing). + +A `<path>` string must use UNIX-style directory seperators (forward +slash `/`), may contain any byte other than `LF`, and must not +start with double quote (`"`). + +If an `LF` or double quote must be encoded into `<path>` shell-style +quoting should be used, e.g. `"path/with\n and \" in it"`. + +The value of `<path>` must be in canoncial form. That is it must not: + +* contain an empty directory component (e.g. `foo//bar` is invalid), +* end with a directory seperator (e.g. `foo/` is invalid), +* start with a directory seperator (e.g. `/foo` is invalid), +* contain the special component `.` or `..` (e.g. `foo/./bar` and + `foo/../bar` are invalid). + +It is recommended that `<path>` always be encoded using UTF-8. + +`filedelete` +^^^^^^^^^^^^ +Included in a `commit` command to remove a file from the branch. +If the file removal makes its directory empty, the directory will +be automatically removed too. This cascades up the tree until the +first non-empty directory or the root is reached. + +.... + 'D' SP <path> LF +.... + +here `<path>` is the complete path of the file to be removed. +See `filemodify` above for a detailed description of `<path>`. + +`mark` +~~~~~~ +Arranges for gfi to save a reference to the current object, allowing +the frontend to recall this object at a future point in time, without +knowing its SHA-1. Here the current object is the object creation +command the `mark` command appears within. This can be `commit`, +`tag`, and `blob`, but `commit` is the most common usage. + +.... + 'mark' SP ':' <idnum> LF +.... + +where `<idnum>` is the number assigned by the frontend to this mark. +The value of `<idnum>` is expressed as an ASCII decimal integer. +The value 0 is reserved and cannot be used as +a mark. Only values greater than or equal to 1 may be used as marks. + +New marks are created automatically. Existing marks can be moved +to another object simply by reusing the same `<idnum>` in another +`mark` command. + +`tag` +~~~~~ +Creates an annotated tag referring to a specific commit. To create +lightweight (non-annotated) tags see the `reset` command below. + +.... + 'tag' SP <name> LF + 'from' SP <committish> LF + 'tagger' SP <name> SP LT <email> GT SP <when> LF + data + LF +.... + +where `<name>` is the name of the tag to create. + +Tag names are automatically prefixed with `refs/tags/` when stored +in Git, so importing the CVS branch symbol `RELENG-1_0-FINAL` would +use just `RELENG-1_0-FINAL` for `<name>`, and gfi will write the +corresponding ref as `refs/tags/RELENG-1_0-FINAL`. + +The value of `<name>` must be a valid refname in Git and therefore +may contain forward slashes. As `LF` is not valid in a Git refname, +no quoting or escaping syntax is supported here. + +The `from` command is the same as in the `commit` command; see +above for details. + +The `tagger` command uses the same format as `committer` within +`commit`; again see above for details. + +The `data` command following `tagger` must supply the annotated tag +message (see below for `data` command syntax). To import an empty +tag message use a 0 length data. Tag messages are free-form and are +not interpreted by Git. Currently they must be encoded in UTF-8, +as gfi does not permit other encodings to be specified. + +Signing annotated tags during import from within gfi is not +supported. Trying to include your own PGP/GPG signature is not +recommended, as the frontend does not (easily) have access to the +complete set of bytes which normally goes into such a signature. +If signing is required, create lightweight tags from within gfi with +`reset`, then create the annotated versions of those tags offline +with the standard gitlink:git-tag[1] process. + +`reset` +~~~~~~~ +Creates (or recreates) the named branch, optionally starting from +a specific revision. The reset command allows a frontend to issue +a new `from` command for an existing branch, or to create a new +branch from an existing commit without creating a new commit. + +.... + 'reset' SP <ref> LF + ('from' SP <committish> LF)? + LF +.... + +For a detailed description of `<ref>` and `<committish>` see above +under `commit` and `from`. + +The `reset` command can also be used to create lightweight +(non-annotated) tags. For example: + +==== + reset refs/tags/938 + from :938 +==== + +would create the lightweight tag `refs/tags/938` referring to +whatever commit mark `:938` references. + +`blob` +~~~~~~ +Requests writing one file revision to the packfile. The revision +is not connected to any commit; this connection must be formed in +a subsequent `commit` command by referencing the blob through an +assigned mark. + +.... + 'blob' LF + mark? + data +.... + +The mark command is optional here as some frontends have chosen +to generate the Git SHA-1 for the blob on their own, and feed that +directly to `commit`. This is typically more work than its worth +however, as marks are inexpensive to store and easy to use. + +`data` +~~~~~~ +Supplies raw data (for use as blob/file content, commit messages, or +annotated tag messages) to gfi. Data can be supplied using an exact +byte count or delimited with a terminating line. Real frontends +intended for production-quality conversions should always use the +exact byte count format, as it is more robust and performs better. +The delimited format is intended primarily for testing gfi. + +Exact byte count format:: + The frontend must specify the number of bytes of data. ++ +.... + 'data' SP <count> LF + <raw> LF +.... ++ +where `<count>` is the exact number of bytes appearing within +`<raw>`. The value of `<count>` is expressed as an ASCII decimal +integer. The `LF` on either side of `<raw>` is not +included in `<count>` and will not be included in the imported data. + +Delimited format:: + A delimiter string is used to mark the end of the data. + gfi will compute the length by searching for the delimiter. + This format is primarly useful for testing and is not + recommended for real data. ++ +.... + 'data' SP '<<' <delim> LF + <raw> LF + <delim> LF +.... ++ +where `<delim>` is the chosen delimiter string. The string `<delim>` +must not appear on a line by itself within `<raw>`, as otherwise +gfi will think the data ends earlier than it really does. The `LF` +immediately trailing `<raw>` is part of `<raw>`. This is one of +the limitations of the delimited format, it is impossible to supply +a data chunk which does not have an LF as its last byte. + +`checkpoint` +~~~~~~~~~~~~ +Forces gfi to close the current packfile and start a new one. +As this requires a significant amount of CPU time and disk IO +(to compute the overall pack SHA-1 checksum and generate the +corresponding index file) it can easily take several minutes for +a single `checkpoint` command to complete. + +.... + 'checkpoint' LF + LF +.... + +Packfile Optimization +--------------------- +When packing a blob gfi always attempts to deltify against the last +blob written. Unless specifically arranged for by the frontend, +this will probably not be a prior version of the same file, so the +generated delta will not be the smallest possible. The resulting +packfile will be compressed, but will not be optimal. + +Frontends which have efficient access to all revisions of a +single file (for example reading an RCS/CVS ,v file) can choose +to supply all revisions of that file as a sequence of consecutive +`blob` commands. This allows gfi to deltify the different file +revisions against each other, saving space in the final packfile. +Marks can be used to later identify individual file revisions during +a sequence of `commit` commands. + +The packfile(s) created by gfi do not encourage good disk access +patterns. This is caused by gfi writing the data in the order +it is received on standard input, while Git typically organizes +data within packfiles to make the most recent (current tip) data +appear before historical data. Git also clusters commits together, +speeding up revision traversal through better cache locality. + +For this reason it is strongly recommended that users repack the +repository with `git repack -a -d` after gfi completes, allowing +Git to reorganize the packfiles for faster data access. If blob +deltas are suboptimal (see above) then also adding the `-f` option +to force recomputation of all deltas can significantly reduce the +final packfile size (30-50% smaller can be quite typical). + +Memory Utilization +------------------ +There are a number of factors which affect how much memory gfi +requires to perform an import. Like critical sections of core +Git, gfi uses its own memory allocators to ammortize any overheads +associated with malloc. In practice gfi tends to ammoritize any +malloc overheads to 0, due to its use of large block allocations. + +per object +~~~~~~~~~~ +gfi maintains an in-memory structure for every object written in +this execution. On a 32 bit system the structure is 32 bytes, +on a 64 bit system the structure is 40 bytes (due to the larger +pointer sizes). Objects in the table are not deallocated until +gfi terminates. Importing 2 million objects on a 32 bit system +will require approximately 64 MiB of memory. + +The object table is actually a hashtable keyed on the object name +(the unique SHA-1). This storage configuration allows gfi to reuse +an existing or already written object and avoid writing duplicates +to the output packfile. Duplicate blobs are surprisingly common +in an import, typically due to branch merges in the source. + +per mark +~~~~~~~~ +Marks are stored in a sparse array, using 1 pointer (4 bytes or 8 +bytes, depending on pointer size) per mark. Although the array +is sparse, frontends are still strongly encouraged to use marks +between 1 and n, where n is the total number of marks required for +this import. + +per branch +~~~~~~~~~~ +Branches are classified as active and inactive. The memory usage +of the two classes is significantly different. + +Inactive branches are stored in a structure which uses 96 or 120 +bytes (32 bit or 64 bit systems, respectively), plus the length of +the branch name (typically under 200 bytes), per branch. gfi will +easily handle as many as 10,000 inactive branches in under 2 MiB +of memory. + +Active branches have the same overhead as inactive branches, but +also contain copies of every tree that has been recently modified on +that branch. If subtree `include` has not been modified since the +branch became active, its contents will not be loaded into memory, +but if subtree `src` has been modified by a commit since the branch +became active, then its contents will be loaded in memory. + +As active branches store metadata about the files contained on that +branch, their in-memory storage size can grow to a considerable size +(see below). + +gfi automatically moves active branches to inactive status based on +a simple least-recently-used algorithm. The LRU chain is updated on +each `commit` command. The maximum number of active branches can be +increased or decreased on the command line with `--active-branches=`. + +per active tree +~~~~~~~~~~~~~~~ +Trees (aka directories) use just 12 bytes of memory on top of the +memory required for their entries (see ``per active file'' below). +The cost of a tree is virtually 0, as its overhead ammortizes out +over the individual file entries. + +per active file entry +~~~~~~~~~~~~~~~~~~~~~ +Files (and pointers to subtrees) within active trees require 52 or 64 +bytes (32/64 bit platforms) per entry. To conserve space, file and +tree names are pooled in a common string table, allowing the filename +``Makefile'' to use just 16 bytes (after including the string header +overhead) no matter how many times it occurs within the project. + +The active branch LRU, when coupled with the filename string pool +and lazy loading of subtrees, allows gfi to efficiently import +projects with 2,000+ branches and 45,114+ files in a very limited +memory footprint (less than 2.7 MiB per active branch). + + +Author +------ +Written by Shawn O. Pearce <spearce@spearce.org>. + +Documentation +-------------- +Documentation by Shawn O. Pearce <spearce@spearce.org>. + +GIT +--- +Part of the gitlink:git[7] suite + @@ -194,6 +194,7 @@ SCRIPTS = $(patsubst %.sh,%,$(SCRIPT_SH)) \ PROGRAMS = \ git-convert-objects$X git-fetch-pack$X git-fsck$X \ git-hash-object$X git-index-pack$X git-local-fetch$X \ + git-fast-import$X \ git-merge-base$X \ git-daemon$X \ git-merge-index$X git-mktag$X git-mktree$X git-patch-id$X \ diff --git a/fast-import.c b/fast-import.c new file mode 100644 index 0000000000..c72c5c7a94 --- /dev/null +++ b/fast-import.c @@ -0,0 +1,2039 @@ +/* +Format of STDIN stream: + + stream ::= cmd*; + + cmd ::= new_blob + | new_commit + | new_tag + | reset_branch + | checkpoint + ; + + new_blob ::= 'blob' lf + mark? + file_content; + file_content ::= data; + + new_commit ::= 'commit' sp ref_str lf + mark? + ('author' sp name '<' email '>' when lf)? + 'committer' sp name '<' email '>' when lf + commit_msg + ('from' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf)? + ('merge' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf)* + file_change* + lf; + commit_msg ::= data; + + file_change ::= file_del | file_obm | file_inm; + file_del ::= 'D' sp path_str lf; + file_obm ::= 'M' sp mode sp (hexsha1 | idnum) sp path_str lf; + file_inm ::= 'M' sp mode sp 'inline' sp path_str lf + data; + + new_tag ::= 'tag' sp tag_str lf + 'from' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf + 'tagger' sp name '<' email '>' when lf + tag_msg; + tag_msg ::= data; + + reset_branch ::= 'reset' sp ref_str lf + ('from' sp (ref_str | hexsha1 | sha1exp_str | idnum) lf)? + lf; + + checkpoint ::= 'checkpoint' lf + lf; + + # note: the first idnum in a stream should be 1 and subsequent + # idnums should not have gaps between values as this will cause + # the stream parser to reserve space for the gapped values. An + # idnum can be updated in the future to a new object by issuing + # a new mark directive with the old idnum. + # + mark ::= 'mark' sp idnum lf; + data ::= (delimited_data | exact_data) + lf; + + # note: delim may be any string but must not contain lf. + # data_line may contain any data but must not be exactly + # delim. + delimited_data ::= 'data' sp '<<' delim lf + (data_line lf)* + delim lf; + + # note: declen indicates the length of binary_data in bytes. + # declen does not include the lf preceeding the binary data. + # + exact_data ::= 'data' sp declen lf + binary_data; + + # note: quoted strings are C-style quoting supporting \c for + # common escapes of 'c' (e..g \n, \t, \\, \") or \nnn where nnn + # is the signed byte value in octal. Note that the only + # characters which must actually be escaped to protect the + # stream formatting is: \, " and LF. Otherwise these values + # are UTF8. + # + ref_str ::= ref; + sha1exp_str ::= sha1exp; + tag_str ::= tag; + path_str ::= path | '"' quoted(path) '"' ; + mode ::= '100644' | '644' + | '100755' | '755' + | '120000' + ; + + declen ::= # unsigned 32 bit value, ascii base10 notation; + bigint ::= # unsigned integer value, ascii base10 notation; + binary_data ::= # file content, not interpreted; + + when ::= raw_when | rfc2822_when; + raw_when ::= ts sp tz; + rfc2822_when ::= # Valid RFC 2822 date and time; + + sp ::= # ASCII space character; + lf ::= # ASCII newline (LF) character; + + # note: a colon (':') must precede the numerical value assigned to + # an idnum. This is to distinguish it from a ref or tag name as + # GIT does not permit ':' in ref or tag strings. + # + idnum ::= ':' bigint; + path ::= # GIT style file path, e.g. "a/b/c"; + ref ::= # GIT ref name, e.g. "refs/heads/MOZ_GECKO_EXPERIMENT"; + tag ::= # GIT tag name, e.g. "FIREFOX_1_5"; + sha1exp ::= # Any valid GIT SHA1 expression; + hexsha1 ::= # SHA1 in hexadecimal format; + + # note: name and email are UTF8 strings, however name must not + # contain '<' or lf and email must not contain any of the + # following: '<', '>', lf. + # + name ::= # valid GIT author/committer name; + email ::= # valid GIT author/committer email; + ts ::= # time since the epoch in seconds, ascii base10 notation; + tz ::= # GIT style timezone; +*/ + +#include "builtin.h" +#include "cache.h" +#include "object.h" +#include "blob.h" +#include "tree.h" +#include "commit.h" +#include "delta.h" +#include "pack.h" +#include "refs.h" +#include "csum-file.h" +#include "strbuf.h" +#include "quote.h" + +#define PACK_ID_BITS 16 +#define MAX_PACK_ID ((1<<PACK_ID_BITS)-1) + +struct object_entry +{ + struct object_entry *next; + uint32_t offset; + unsigned type : TYPE_BITS; + unsigned pack_id : PACK_ID_BITS; + unsigned char sha1[20]; +}; + +struct object_entry_pool +{ + struct object_entry_pool *next_pool; + struct object_entry *next_free; + struct object_entry *end; + struct object_entry entries[FLEX_ARRAY]; /* more */ +}; + +struct mark_set +{ + union { + struct object_entry *marked[1024]; + struct mark_set *sets[1024]; + } data; + unsigned int shift; +}; + +struct last_object +{ + void *data; + unsigned long len; + uint32_t offset; + unsigned int depth; + unsigned no_free:1; +}; + +struct mem_pool +{ + struct mem_pool *next_pool; + char *next_free; + char *end; + char space[FLEX_ARRAY]; /* more */ +}; + +struct atom_str +{ + struct atom_str *next_atom; + unsigned short str_len; + char str_dat[FLEX_ARRAY]; /* more */ +}; + +struct tree_content; +struct tree_entry +{ + struct tree_content *tree; + struct atom_str* name; + struct tree_entry_ms + { + uint16_t mode; + unsigned char sha1[20]; + } versions[2]; +}; + +struct tree_content +{ + unsigned int entry_capacity; /* must match avail_tree_content */ + unsigned int entry_count; + unsigned int delta_depth; + struct tree_entry *entries[FLEX_ARRAY]; /* more */ +}; + +struct avail_tree_content +{ + unsigned int entry_capacity; /* must match tree_content */ + struct avail_tree_content *next_avail; +}; + +struct branch +{ + struct branch *table_next_branch; + struct branch *active_next_branch; + const char *name; + struct tree_entry branch_tree; + uintmax_t last_commit; + unsigned int pack_id; + unsigned char sha1[20]; +}; + +struct tag +{ + struct tag *next_tag; + const char *name; + unsigned int pack_id; + unsigned char sha1[20]; +}; + +struct dbuf +{ + void *buffer; + size_t capacity; +}; + +struct hash_list +{ + struct hash_list *next; + unsigned char sha1[20]; +}; + +typedef enum { + WHENSPEC_RAW = 1, + WHENSPEC_RFC2822, + WHENSPEC_NOW, +} whenspec_type; + +/* Configured limits on output */ +static unsigned long max_depth = 10; +static unsigned long max_packsize = (1LL << 32) - 1; +static int force_update; + +/* Stats and misc. counters */ +static uintmax_t alloc_count; +static uintmax_t marks_set_count; +static uintmax_t object_count_by_type[1 << TYPE_BITS]; +static uintmax_t duplicate_count_by_type[1 << TYPE_BITS]; +static uintmax_t delta_count_by_type[1 << TYPE_BITS]; +static unsigned long object_count; +static unsigned long branch_count; +static unsigned long branch_load_count; +static int failure; + +/* Memory pools */ +static size_t mem_pool_alloc = 2*1024*1024 - sizeof(struct mem_pool); +static size_t total_allocd; +static struct mem_pool *mem_pool; + +/* Atom management */ +static unsigned int atom_table_sz = 4451; +static unsigned int atom_cnt; +static struct atom_str **atom_table; + +/* The .pack file being generated */ +static unsigned int pack_id; +static struct packed_git *pack_data; +static struct packed_git **all_packs; +static unsigned long pack_size; + +/* Table of objects we've written. */ +static unsigned int object_entry_alloc = 5000; +static struct object_entry_pool *blocks; +static struct object_entry *object_table[1 << 16]; +static struct mark_set *marks; +static const char* mark_file; + +/* Our last blob */ +static struct last_object last_blob; + +/* Tree management */ +static unsigned int tree_entry_alloc = 1000; +static void *avail_tree_entry; +static unsigned int avail_tree_table_sz = 100; +static struct avail_tree_content **avail_tree_table; +static struct dbuf old_tree; +static struct dbuf new_tree; + +/* Branch data */ +static unsigned long max_active_branches = 5; +static unsigned long cur_active_branches; +static unsigned long branch_table_sz = 1039; +static struct branch **branch_table; +static struct branch *active_branches; + +/* Tag data */ +static struct tag *first_tag; +static struct tag *last_tag; + +/* Input stream parsing */ +static whenspec_type whenspec = WHENSPEC_RAW; +static struct strbuf command_buf; +static uintmax_t next_mark; +static struct dbuf new_data; + + +static void alloc_objects(unsigned int cnt) +{ + struct object_entry_pool *b; + + b = xmalloc(sizeof(struct object_entry_pool) + + cnt * sizeof(struct object_entry)); + b->next_pool = blocks; + b->next_free = b->entries; + b->end = b->entries + cnt; + blocks = b; + alloc_count += cnt; +} + +static struct object_entry *new_object(unsigned char *sha1) +{ + struct object_entry *e; + + if (blocks->next_free == blocks->end) + alloc_objects(object_entry_alloc); + + e = blocks->next_free++; + hashcpy(e->sha1, sha1); + return e; +} + +static struct object_entry *find_object(unsigned char *sha1) +{ + unsigned int h = sha1[0] << 8 | sha1[1]; + struct object_entry *e; + for (e = object_table[h]; e; e = e->next) + if (!hashcmp(sha1, e->sha1)) + return e; + return NULL; +} + +static struct object_entry *insert_object(unsigned char *sha1) +{ + unsigned int h = sha1[0] << 8 | sha1[1]; + struct object_entry *e = object_table[h]; + struct object_entry *p = NULL; + + while (e) { + if (!hashcmp(sha1, e->sha1)) + return e; + p = e; + e = e->next; + } + + e = new_object(sha1); + e->next = NULL; + e->offset = 0; + if (p) + p->next = e; + else + object_table[h] = e; + return e; +} + +static unsigned int hc_str(const char *s, size_t len) +{ + unsigned int r = 0; + while (len-- > 0) + r = r * 31 + *s++; + return r; +} + +static void *pool_alloc(size_t len) +{ + struct mem_pool *p; + void *r; + + for (p = mem_pool; p; p = p->next_pool) + if ((p->end - p->next_free >= len)) + break; + + if (!p) { + if (len >= (mem_pool_alloc/2)) { + total_allocd += len; + return xmalloc(len); + } + total_allocd += sizeof(struct mem_pool) + mem_pool_alloc; + p = xmalloc(sizeof(struct mem_pool) + mem_pool_alloc); + p->next_pool = mem_pool; + p->next_free = p->space; + p->end = p->next_free + mem_pool_alloc; + mem_pool = p; + } + + r = p->next_free; + /* round out to a pointer alignment */ + if (len & (sizeof(void*) - 1)) + len += sizeof(void*) - (len & (sizeof(void*) - 1)); + p->next_free += len; + return r; +} + +static void *pool_calloc(size_t count, size_t size) +{ + size_t len = count * size; + void *r = pool_alloc(len); + memset(r, 0, len); + return r; +} + +static char *pool_strdup(const char *s) +{ + char *r = pool_alloc(strlen(s) + 1); + strcpy(r, s); + return r; +} + +static void size_dbuf(struct dbuf *b, size_t maxlen) +{ + if (b->buffer) { + if (b->capacity >= maxlen) + return; + free(b->buffer); + } + b->capacity = ((maxlen / 1024) + 1) * 1024; + b->buffer = xmalloc(b->capacity); +} + +static void insert_mark(uintmax_t idnum, struct object_entry *oe) +{ + struct mark_set *s = marks; + while ((idnum >> s->shift) >= 1024) { + s = pool_calloc(1, sizeof(struct mark_set)); + s->shift = marks->shift + 10; + s->data.sets[0] = marks; + marks = s; + } + while (s->shift) { + uintmax_t i = idnum >> s->shift; + idnum -= i << s->shift; + if (!s->data.sets[i]) { + s->data.sets[i] = pool_calloc(1, sizeof(struct mark_set)); + s->data.sets[i]->shift = s->shift - 10; + } + s = s->data.sets[i]; + } + if (!s->data.marked[idnum]) + marks_set_count++; + s->data.marked[idnum] = oe; +} + +static struct object_entry *find_mark(uintmax_t idnum) +{ + uintmax_t orig_idnum = idnum; + struct mark_set *s = marks; + struct object_entry *oe = NULL; + if ((idnum >> s->shift) < 1024) { + while (s && s->shift) { + uintmax_t i = idnum >> s->shift; + idnum -= i << s->shift; + s = s->data.sets[i]; + } + if (s) + oe = s->data.marked[idnum]; + } + if (!oe) + die("mark :%ju not declared", orig_idnum); + return oe; +} + +static struct atom_str *to_atom(const char *s, unsigned short len) +{ + unsigned int hc = hc_str(s, len) % atom_table_sz; + struct atom_str *c; + + for (c = atom_table[hc]; c; c = c->next_atom) + if (c->str_len == len && !strncmp(s, c->str_dat, len)) + return c; + + c = pool_alloc(sizeof(struct atom_str) + len + 1); + c->str_len = len; + strncpy(c->str_dat, s, len); + c->str_dat[len] = 0; + c->next_atom = atom_table[hc]; + atom_table[hc] = c; + atom_cnt++; + return c; +} + +static struct branch *lookup_branch(const char *name) +{ + unsigned int hc = hc_str(name, strlen(name)) % branch_table_sz; + struct branch *b; + + for (b = branch_table[hc]; b; b = b->table_next_branch) + if (!strcmp(name, b->name)) + return b; + return NULL; +} + +static struct branch *new_branch(const char *name) +{ + unsigned int hc = hc_str(name, strlen(name)) % branch_table_sz; + struct branch* b = lookup_branch(name); + + if (b) + die("Invalid attempt to create duplicate branch: %s", name); + if (check_ref_format(name)) + die("Branch name doesn't conform to GIT standards: %s", name); + + b = pool_calloc(1, sizeof(struct branch)); + b->name = pool_strdup(name); + b->table_next_branch = branch_table[hc]; + b->branch_tree.versions[0].mode = S_IFDIR; + b->branch_tree.versions[1].mode = S_IFDIR; + b->pack_id = MAX_PACK_ID; + branch_table[hc] = b; + branch_count++; + return b; +} + +static unsigned int hc_entries(unsigned int cnt) +{ + cnt = cnt & 7 ? (cnt / 8) + 1 : cnt / 8; + return cnt < avail_tree_table_sz ? cnt : avail_tree_table_sz - 1; +} + +static struct tree_content *new_tree_content(unsigned int cnt) +{ + struct avail_tree_content *f, *l = NULL; + struct tree_content *t; + unsigned int hc = hc_entries(cnt); + + for (f = avail_tree_table[hc]; f; l = f, f = f->next_avail) + if (f->entry_capacity >= cnt) + break; + + if (f) { + if (l) + l->next_avail = f->next_avail; + else + avail_tree_table[hc] = f->next_avail; + } else { + cnt = cnt & 7 ? ((cnt / 8) + 1) * 8 : cnt; + f = pool_alloc(sizeof(*t) + sizeof(t->entries[0]) * cnt); + f->entry_capacity = cnt; + } + + t = (struct tree_content*)f; + t->entry_count = 0; + t->delta_depth = 0; + return t; +} + +static void release_tree_entry(struct tree_entry *e); +static void release_tree_content(struct tree_content *t) +{ + struct avail_tree_content *f = (struct avail_tree_content*)t; + unsigned int hc = hc_entries(f->entry_capacity); + f->next_avail = avail_tree_table[hc]; + avail_tree_table[hc] = f; +} + +static void release_tree_content_recursive(struct tree_content *t) +{ + unsigned int i; + for (i = 0; i < t->entry_count; i++) + release_tree_entry(t->entries[i]); + release_tree_content(t); +} + +static struct tree_content *grow_tree_content( + struct tree_content *t, + int amt) +{ + struct tree_content *r = new_tree_content(t->entry_count + amt); + r->entry_count = t->entry_count; + r->delta_depth = t->delta_depth; + memcpy(r->entries,t->entries,t->entry_count*sizeof(t->entries[0])); + release_tree_content(t); + return r; +} + +static struct tree_entry *new_tree_entry(void) +{ + struct tree_entry *e; + + if (!avail_tree_entry) { + unsigned int n = tree_entry_alloc; + total_allocd += n * sizeof(struct tree_entry); + avail_tree_entry = e = xmalloc(n * sizeof(struct tree_entry)); + while (n-- > 1) { + *((void**)e) = e + 1; + e++; + } + *((void**)e) = NULL; + } + + e = avail_tree_entry; + avail_tree_entry = *((void**)e); + return e; +} + +static void release_tree_entry(struct tree_entry *e) +{ + if (e->tree) + release_tree_content_recursive(e->tree); + *((void**)e) = avail_tree_entry; + avail_tree_entry = e; +} + +static void start_packfile(void) +{ + static char tmpfile[PATH_MAX]; + struct packed_git *p; + struct pack_header hdr; + int pack_fd; + + snprintf(tmpfile, sizeof(tmpfile), + "%s/pack_XXXXXX", get_object_directory()); + pack_fd = mkstemp(tmpfile); + if (pack_fd < 0) + die("Can't create %s: %s", tmpfile, strerror(errno)); + p = xcalloc(1, sizeof(*p) + strlen(tmpfile) + 2); + strcpy(p->pack_name, tmpfile); + p->pack_fd = pack_fd; + + hdr.hdr_signature = htonl(PACK_SIGNATURE); + hdr.hdr_version = htonl(2); + hdr.hdr_entries = 0; + write_or_die(p->pack_fd, &hdr, sizeof(hdr)); + + pack_data = p; + pack_size = sizeof(hdr); + object_count = 0; + + all_packs = xrealloc(all_packs, sizeof(*all_packs) * (pack_id + 1)); + all_packs[pack_id] = p; +} + +static void fixup_header_footer(void) +{ + static const int buf_sz = 128 * 1024; + int pack_fd = pack_data->pack_fd; + SHA_CTX c; + struct pack_header hdr; + char *buf; + + if (lseek(pack_fd, 0, SEEK_SET) != 0) + die("Failed seeking to start: %s", strerror(errno)); + if (read_in_full(pack_fd, &hdr, sizeof(hdr)) != sizeof(hdr)) + die("Unable to reread header of %s", pack_data->pack_name); + if (lseek(pack_fd, 0, SEEK_SET) != 0) + die("Failed seeking to start: %s", strerror(errno)); + hdr.hdr_entries = htonl(object_count); + write_or_die(pack_fd, &hdr, sizeof(hdr)); + + SHA1_Init(&c); + SHA1_Update(&c, &hdr, sizeof(hdr)); + + buf = xmalloc(buf_sz); + for (;;) { + size_t n = xread(pack_fd, buf, buf_sz); + if (!n) + break; + if (n < 0) + die("Failed to checksum %s", pack_data->pack_name); + SHA1_Update(&c, buf, n); + } + free(buf); + + SHA1_Final(pack_data->sha1, &c); + write_or_die(pack_fd, pack_data->sha1, sizeof(pack_data->sha1)); + close(pack_fd); +} + +static int oecmp (const void *a_, const void *b_) +{ + struct object_entry *a = *((struct object_entry**)a_); + struct object_entry *b = *((struct object_entry**)b_); + return hashcmp(a->sha1, b->sha1); +} + +static char *create_index(void) +{ + static char tmpfile[PATH_MAX]; + SHA_CTX ctx; + struct sha1file *f; + struct object_entry **idx, **c, **last, *e; + struct object_entry_pool *o; + uint32_t array[256]; + int i, idx_fd; + + /* Build the sorted table of object IDs. */ + idx = xmalloc(object_count * sizeof(struct object_entry*)); + c = idx; + for (o = blocks; o; o = o->next_pool) + for (e = o->next_free; e-- != o->entries;) + if (pack_id == e->pack_id) + *c++ = e; + last = idx + object_count; + if (c != last) + die("internal consistency error creating the index"); + qsort(idx, object_count, sizeof(struct object_entry*), oecmp); + + /* Generate the fan-out array. */ + c = idx; + for (i = 0; i < 256; i++) { + struct object_entry **next = c;; + while (next < last) { + if ((*next)->sha1[0] != i) + break; + next++; + } + array[i] = htonl(next - idx); + c = next; + } + + snprintf(tmpfile, sizeof(tmpfile), + "%s/index_XXXXXX", get_object_directory()); + idx_fd = mkstemp(tmpfile); + if (idx_fd < 0) + die("Can't create %s: %s", tmpfile, strerror(errno)); + f = sha1fd(idx_fd, tmpfile); + sha1write(f, array, 256 * sizeof(int)); + SHA1_Init(&ctx); + for (c = idx; c != last; c++) { + uint32_t offset = htonl((*c)->offset); + sha1write(f, &offset, 4); + sha1write(f, (*c)->sha1, sizeof((*c)->sha1)); + SHA1_Update(&ctx, (*c)->sha1, 20); + } + sha1write(f, pack_data->sha1, sizeof(pack_data->sha1)); + sha1close(f, NULL, 1); + free(idx); + SHA1_Final(pack_data->sha1, &ctx); + return tmpfile; +} + +static char *keep_pack(char *curr_index_name) +{ + static char name[PATH_MAX]; + static char *keep_msg = "fast-import"; + int keep_fd; + + chmod(pack_data->pack_name, 0444); + chmod(curr_index_name, 0444); + + snprintf(name, sizeof(name), "%s/pack/pack-%s.keep", + get_object_directory(), sha1_to_hex(pack_data->sha1)); + keep_fd = open(name, O_RDWR|O_CREAT|O_EXCL, 0600); + if (keep_fd < 0) + die("cannot create keep file"); + write(keep_fd, keep_msg, strlen(keep_msg)); + close(keep_fd); + + snprintf(name, sizeof(name), "%s/pack/pack-%s.pack", + get_object_directory(), sha1_to_hex(pack_data->sha1)); + if (move_temp_to_file(pack_data->pack_name, name)) + die("cannot store pack file"); + + snprintf(name, sizeof(name), "%s/pack/pack-%s.idx", + get_object_directory(), sha1_to_hex(pack_data->sha1)); + if (move_temp_to_file(curr_index_name, name)) + die("cannot store index file"); + return name; +} + +static void unkeep_all_packs(void) +{ + static char name[PATH_MAX]; + int k; + + for (k = 0; k < pack_id; k++) { + struct packed_git *p = all_packs[k]; + snprintf(name, sizeof(name), "%s/pack/pack-%s.keep", + get_object_directory(), sha1_to_hex(p->sha1)); + unlink(name); + } +} + +static void end_packfile(void) +{ + struct packed_git *old_p = pack_data, *new_p; + + if (object_count) { + char *idx_name; + int i; + struct branch *b; + struct tag *t; + + fixup_header_footer(); + idx_name = keep_pack(create_index()); + + /* Register the packfile with core git's machinary. */ + new_p = add_packed_git(idx_name, strlen(idx_name), 1); + if (!new_p) + die("core git rejected index %s", idx_name); + new_p->windows = old_p->windows; + all_packs[pack_id] = new_p; + install_packed_git(new_p); + + /* Print the boundary */ + fprintf(stdout, "%s:", new_p->pack_name); + for (i = 0; i < branch_table_sz; i++) { + for (b = branch_table[i]; b; b = b->table_next_branch) { + if (b->pack_id == pack_id) + fprintf(stdout, " %s", sha1_to_hex(b->sha1)); + } + } + for (t = first_tag; t; t = t->next_tag) { + if (t->pack_id == pack_id) + fprintf(stdout, " %s", sha1_to_hex(t->sha1)); + } + fputc('\n', stdout); + + pack_id++; + } + else + unlink(old_p->pack_name); + free(old_p); + + /* We can't carry a delta across packfiles. */ + free(last_blob.data); + last_blob.data = NULL; + last_blob.len = 0; + last_blob.offset = 0; + last_blob.depth = 0; +} + +static void checkpoint(void) +{ + end_packfile(); + start_packfile(); +} + +static size_t encode_header( + enum object_type type, + size_t size, + unsigned char *hdr) +{ + int n = 1; + unsigned char c; + + if (type < OBJ_COMMIT || type > OBJ_REF_DELTA) + die("bad type %d", type); + + c = (type << 4) | (size & 15); + size >>= 4; + while (size) { + *hdr++ = c | 0x80; + c = size & 0x7f; + size >>= 7; + n++; + } + *hdr = c; + return n; +} + +static int store_object( + enum object_type type, + void *dat, + size_t datlen, + struct last_object *last, + unsigned char *sha1out, + uintmax_t mark) +{ + void *out, *delta; + struct object_entry *e; + unsigned char hdr[96]; + unsigned char sha1[20]; + unsigned long hdrlen, deltalen; + SHA_CTX c; + z_stream s; + + hdrlen = sprintf((char*)hdr,"%s %lu",type_names[type],datlen) + 1; + SHA1_Init(&c); + SHA1_Update(&c, hdr, hdrlen); + SHA1_Update(&c, dat, datlen); + SHA1_Final(sha1, &c); + if (sha1out) + hashcpy(sha1out, sha1); + + e = insert_object(sha1); + if (mark) + insert_mark(mark, e); + if (e->offset) { + duplicate_count_by_type[type]++; + return 1; + } + + if (last && last->data && last->depth < max_depth) { + delta = diff_delta(last->data, last->len, + dat, datlen, + &deltalen, 0); + if (delta && deltalen >= datlen) { + free(delta); + delta = NULL; + } + } else + delta = NULL; + + memset(&s, 0, sizeof(s)); + deflateInit(&s, zlib_compression_level); + if (delta) { + s.next_in = delta; + s.avail_in = deltalen; + } else { + s.next_in = dat; + s.avail_in = datlen; + } + s.avail_out = deflateBound(&s, s.avail_in); + s.next_out = out = xmalloc(s.avail_out); + while (deflate(&s, Z_FINISH) == Z_OK) + /* nothing */; + deflateEnd(&s); + + /* Determine if we should auto-checkpoint. */ + if ((pack_size + 60 + s.total_out) > max_packsize + || (pack_size + 60 + s.total_out) < pack_size) { + + /* This new object needs to *not* have the current pack_id. */ + e->pack_id = pack_id + 1; + checkpoint(); + + /* We cannot carry a delta into the new pack. */ + if (delta) { + free(delta); + delta = NULL; + + memset(&s, 0, sizeof(s)); + deflateInit(&s, zlib_compression_level); + s.next_in = dat; + s.avail_in = datlen; + s.avail_out = deflateBound(&s, s.avail_in); + s.next_out = out = xrealloc(out, s.avail_out); + while (deflate(&s, Z_FINISH) == Z_OK) + /* nothing */; + deflateEnd(&s); + } + } + + e->type = type; + e->pack_id = pack_id; + e->offset = pack_size; + object_count++; + object_count_by_type[type]++; + + if (delta) { + unsigned long ofs = e->offset - last->offset; + unsigned pos = sizeof(hdr) - 1; + + delta_count_by_type[type]++; + last->depth++; + + hdrlen = encode_header(OBJ_OFS_DELTA, deltalen, hdr); + write_or_die(pack_data->pack_fd, hdr, hdrlen); + pack_size += hdrlen; + + hdr[pos] = ofs & 127; + while (ofs >>= 7) + hdr[--pos] = 128 | (--ofs & 127); + write_or_die(pack_data->pack_fd, hdr + pos, sizeof(hdr) - pos); + pack_size += sizeof(hdr) - pos; + } else { + if (last) + last->depth = 0; + hdrlen = encode_header(type, datlen, hdr); + write_or_die(pack_data->pack_fd, hdr, hdrlen); + pack_size += hdrlen; + } + + write_or_die(pack_data->pack_fd, out, s.total_out); + pack_size += s.total_out; + + free(out); + free(delta); + if (last) { + if (!last->no_free) + free(last->data); + last->data = dat; + last->offset = e->offset; + last->len = datlen; + } + return 0; +} + +static void *gfi_unpack_entry( + struct object_entry *oe, + unsigned long *sizep) +{ + static char type[20]; + struct packed_git *p = all_packs[oe->pack_id]; + if (p == pack_data) + p->pack_size = pack_size + 20; + return unpack_entry(p, oe->offset, type, sizep); +} + +static const char *get_mode(const char *str, uint16_t *modep) +{ + unsigned char c; + uint16_t mode = 0; + + while ((c = *str++) != ' ') { + if (c < '0' || c > '7') + return NULL; + mode = (mode << 3) + (c - '0'); + } + *modep = mode; + return str; +} + +static void load_tree(struct tree_entry *root) +{ + unsigned char* sha1 = root->versions[1].sha1; + struct object_entry *myoe; + struct tree_content *t; + unsigned long size; + char *buf; + const char *c; + + root->tree = t = new_tree_content(8); + if (is_null_sha1(sha1)) + return; + + myoe = find_object(sha1); + if (myoe) { + if (myoe->type != OBJ_TREE) + die("Not a tree: %s", sha1_to_hex(sha1)); + t->delta_depth = 0; + buf = gfi_unpack_entry(myoe, &size); + } else { + char type[20]; + buf = read_sha1_file(sha1, type, &size); + if (!buf || strcmp(type, tree_type)) + die("Can't load tree %s", sha1_to_hex(sha1)); + } + + c = buf; + while (c != (buf + size)) { + struct tree_entry *e = new_tree_entry(); + + if (t->entry_count == t->entry_capacity) + root->tree = t = grow_tree_content(t, 8); + t->entries[t->entry_count++] = e; + + e->tree = NULL; + c = get_mode(c, &e->versions[1].mode); + if (!c) + die("Corrupt mode in %s", sha1_to_hex(sha1)); + e->versions[0].mode = e->versions[1].mode; + e->name = to_atom(c, (unsigned short)strlen(c)); + c += e->name->str_len + 1; + hashcpy(e->versions[0].sha1, (unsigned char*)c); + hashcpy(e->versions[1].sha1, (unsigned char*)c); + c += 20; + } + free(buf); +} + +static int tecmp0 (const void *_a, const void *_b) +{ + struct tree_entry *a = *((struct tree_entry**)_a); + struct tree_entry *b = *((struct tree_entry**)_b); + return base_name_compare( + a->name->str_dat, a->name->str_len, a->versions[0].mode, + b->name->str_dat, b->name->str_len, b->versions[0].mode); +} + +static int tecmp1 (const void *_a, const void *_b) +{ + struct tree_entry *a = *((struct tree_entry**)_a); + struct tree_entry *b = *((struct tree_entry**)_b); + return base_name_compare( + a->name->str_dat, a->name->str_len, a->versions[1].mode, + b->name->str_dat, b->name->str_len, b->versions[1].mode); +} + +static void mktree(struct tree_content *t, + int v, + unsigned long *szp, + struct dbuf *b) +{ + size_t maxlen = 0; + unsigned int i; + char *c; + + if (!v) + qsort(t->entries,t->entry_count,sizeof(t->entries[0]),tecmp0); + else + qsort(t->entries,t->entry_count,sizeof(t->entries[0]),tecmp1); + + for (i = 0; i < t->entry_count; i++) { + if (t->entries[i]->versions[v].mode) + maxlen += t->entries[i]->name->str_len + 34; + } + + size_dbuf(b, maxlen); + c = b->buffer; + for (i = 0; i < t->entry_count; i++) { + struct tree_entry *e = t->entries[i]; + if (!e->versions[v].mode) + continue; + c += sprintf(c, "%o", (unsigned int)e->versions[v].mode); + *c++ = ' '; + strcpy(c, e->name->str_dat); + c += e->name->str_len + 1; + hashcpy((unsigned char*)c, e->versions[v].sha1); + c += 20; + } + *szp = c - (char*)b->buffer; +} + +static void store_tree(struct tree_entry *root) +{ + struct tree_content *t = root->tree; + unsigned int i, j, del; + unsigned long new_len; + struct last_object lo; + struct object_entry *le; + + if (!is_null_sha1(root->versions[1].sha1)) + return; + + for (i = 0; i < t->entry_count; i++) { + if (t->entries[i]->tree) + store_tree(t->entries[i]); + } + + le = find_object(root->versions[0].sha1); + if (!S_ISDIR(root->versions[0].mode) + || !le + || le->pack_id != pack_id) { + lo.data = NULL; + lo.depth = 0; + } else { + mktree(t, 0, &lo.len, &old_tree); + lo.data = old_tree.buffer; + lo.offset = le->offset; + lo.depth = t->delta_depth; + lo.no_free = 1; + } + + mktree(t, 1, &new_len, &new_tree); + store_object(OBJ_TREE, new_tree.buffer, new_len, + &lo, root->versions[1].sha1, 0); + + t->delta_depth = lo.depth; + for (i = 0, j = 0, del = 0; i < t->entry_count; i++) { + struct tree_entry *e = t->entries[i]; + if (e->versions[1].mode) { + e->versions[0].mode = e->versions[1].mode; + hashcpy(e->versions[0].sha1, e->versions[1].sha1); + t->entries[j++] = e; + } else { + release_tree_entry(e); + del++; + } + } + t->entry_count -= del; +} + +static int tree_content_set( + struct tree_entry *root, + const char *p, + const unsigned char *sha1, + const uint16_t mode) +{ + struct tree_content *t = root->tree; + const char *slash1; + unsigned int i, n; + struct tree_entry *e; + + slash1 = strchr(p, '/'); + if (slash1) + n = slash1 - p; + else + n = strlen(p); + + for (i = 0; i < t->entry_count; i++) { + e = t->entries[i]; + if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) { + if (!slash1) { + if (e->versions[1].mode == mode + && !hashcmp(e->versions[1].sha1, sha1)) + return 0; + e->versions[1].mode = mode; + hashcpy(e->versions[1].sha1, sha1); + if (e->tree) { + release_tree_content_recursive(e->tree); + e->tree = NULL; + } + hashclr(root->versions[1].sha1); + return 1; + } + if (!S_ISDIR(e->versions[1].mode)) { + e->tree = new_tree_content(8); + e->versions[1].mode = S_IFDIR; + } + if (!e->tree) + load_tree(e); + if (tree_content_set(e, slash1 + 1, sha1, mode)) { + hashclr(root->versions[1].sha1); + return 1; + } + return 0; + } + } + + if (t->entry_count == t->entry_capacity) + root->tree = t = grow_tree_content(t, 8); + e = new_tree_entry(); + e->name = to_atom(p, (unsigned short)n); + e->versions[0].mode = 0; + hashclr(e->versions[0].sha1); + t->entries[t->entry_count++] = e; + if (slash1) { + e->tree = new_tree_content(8); + e->versions[1].mode = S_IFDIR; + tree_content_set(e, slash1 + 1, sha1, mode); + } else { + e->tree = NULL; + e->versions[1].mode = mode; + hashcpy(e->versions[1].sha1, sha1); + } + hashclr(root->versions[1].sha1); + return 1; +} + +static int tree_content_remove(struct tree_entry *root, const char *p) +{ + struct tree_content *t = root->tree; + const char *slash1; + unsigned int i, n; + struct tree_entry *e; + + slash1 = strchr(p, '/'); + if (slash1) + n = slash1 - p; + else + n = strlen(p); + + for (i = 0; i < t->entry_count; i++) { + e = t->entries[i]; + if (e->name->str_len == n && !strncmp(p, e->name->str_dat, n)) { + if (!slash1 || !S_ISDIR(e->versions[1].mode)) + goto del_entry; + if (!e->tree) + load_tree(e); + if (tree_content_remove(e, slash1 + 1)) { + for (n = 0; n < e->tree->entry_count; n++) { + if (e->tree->entries[n]->versions[1].mode) { + hashclr(root->versions[1].sha1); + return 1; + } + } + goto del_entry; + } + return 0; + } + } + return 0; + +del_entry: + if (e->tree) { + release_tree_content_recursive(e->tree); + e->tree = NULL; + } + e->versions[1].mode = 0; + hashclr(e->versions[1].sha1); + hashclr(root->versions[1].sha1); + return 1; +} + +static int update_branch(struct branch *b) +{ + static const char *msg = "fast-import"; + struct ref_lock *lock; + unsigned char old_sha1[20]; + + if (read_ref(b->name, old_sha1)) + hashclr(old_sha1); + lock = lock_any_ref_for_update(b->name, old_sha1); + if (!lock) + return error("Unable to lock %s", b->name); + if (!force_update && !is_null_sha1(old_sha1)) { + struct commit *old_cmit, *new_cmit; + + old_cmit = lookup_commit_reference_gently(old_sha1, 0); + new_cmit = lookup_commit_reference_gently(b->sha1, 0); + if (!old_cmit || !new_cmit) { + unlock_ref(lock); + return error("Branch %s is missing commits.", b->name); + } + + if (!in_merge_bases(old_cmit, new_cmit)) { + unlock_ref(lock); + warn("Not updating %s" + " (new tip %s does not contain %s)", + b->name, sha1_to_hex(b->sha1), sha1_to_hex(old_sha1)); + return -1; + } + } + if (write_ref_sha1(lock, b->sha1, msg) < 0) + return error("Unable to update %s", b->name); + return 0; +} + +static void dump_branches(void) +{ + unsigned int i; + struct branch *b; + + for (i = 0; i < branch_table_sz; i++) { + for (b = branch_table[i]; b; b = b->table_next_branch) + failure |= update_branch(b); + } +} + +static void dump_tags(void) +{ + static const char *msg = "fast-import"; + struct tag *t; + struct ref_lock *lock; + char ref_name[PATH_MAX]; + + for (t = first_tag; t; t = t->next_tag) { + sprintf(ref_name, "tags/%s", t->name); + lock = lock_ref_sha1(ref_name, NULL); + if (!lock || write_ref_sha1(lock, t->sha1, msg) < 0) + failure |= error("Unable to update %s", ref_name); + } +} + +static void dump_marks_helper(FILE *f, + uintmax_t base, + struct mark_set *m) +{ + uintmax_t k; + if (m->shift) { + for (k = 0; k < 1024; k++) { + if (m->data.sets[k]) + dump_marks_helper(f, (base + k) << m->shift, + m->data.sets[k]); + } + } else { + for (k = 0; k < 1024; k++) { + if (m->data.marked[k]) + fprintf(f, ":%ju %s\n", base + k, + sha1_to_hex(m->data.marked[k]->sha1)); + } + } +} + +static void dump_marks(void) +{ + if (mark_file) + { + FILE *f = fopen(mark_file, "w"); + dump_marks_helper(f, 0, marks); + fclose(f); + } +} + +static void read_next_command(void) +{ + read_line(&command_buf, stdin, '\n'); +} + +static void cmd_mark(void) +{ + if (!strncmp("mark :", command_buf.buf, 6)) { + next_mark = strtoumax(command_buf.buf + 6, NULL, 10); + read_next_command(); + } + else + next_mark = 0; +} + +static void *cmd_data (size_t *size) +{ + size_t length; + char *buffer; + + if (strncmp("data ", command_buf.buf, 5)) + die("Expected 'data n' command, found: %s", command_buf.buf); + + if (!strncmp("<<", command_buf.buf + 5, 2)) { + char *term = xstrdup(command_buf.buf + 5 + 2); + size_t sz = 8192, term_len = command_buf.len - 5 - 2; + length = 0; + buffer = xmalloc(sz); + for (;;) { + read_next_command(); + if (command_buf.eof) + die("EOF in data (terminator '%s' not found)", term); + if (term_len == command_buf.len + && !strcmp(term, command_buf.buf)) + break; + if (sz < (length + command_buf.len)) { + sz = sz * 3 / 2 + 16; + if (sz < (length + command_buf.len)) + sz = length + command_buf.len; + buffer = xrealloc(buffer, sz); + } + memcpy(buffer + length, + command_buf.buf, + command_buf.len - 1); + length += command_buf.len - 1; + buffer[length++] = '\n'; + } + free(term); + } + else { + size_t n = 0; + length = strtoul(command_buf.buf + 5, NULL, 10); + buffer = xmalloc(length); + while (n < length) { + size_t s = fread(buffer + n, 1, length - n, stdin); + if (!s && feof(stdin)) + die("EOF in data (%lu bytes remaining)", length - n); + n += s; + } + } + + if (fgetc(stdin) != '\n') + die("An lf did not trail the binary data as expected."); + + *size = length; + return buffer; +} + +static int validate_raw_date(const char *src, char *result, int maxlen) +{ + const char *orig_src = src; + char *endp, sign; + + strtoul(src, &endp, 10); + if (endp == src || *endp != ' ') + return -1; + + src = endp + 1; + if (*src != '-' && *src != '+') + return -1; + sign = *src; + + strtoul(src + 1, &endp, 10); + if (endp == src || *endp || (endp - orig_src) >= maxlen) + return -1; + + strcpy(result, orig_src); + return 0; +} + +static char *parse_ident(const char *buf) +{ + const char *gt; + size_t name_len; + char *ident; + + gt = strrchr(buf, '>'); + if (!gt) + die("Missing > in ident string: %s", buf); + gt++; + if (*gt != ' ') + die("Missing space after > in ident string: %s", buf); + gt++; + name_len = gt - buf; + ident = xmalloc(name_len + 24); + strncpy(ident, buf, name_len); + + switch (whenspec) { + case WHENSPEC_RAW: + if (validate_raw_date(gt, ident + name_len, 24) < 0) + die("Invalid raw date \"%s\" in ident: %s", gt, buf); + break; + case WHENSPEC_RFC2822: + if (parse_date(gt, ident + name_len, 24) < 0) + die("Invalid rfc2822 date \"%s\" in ident: %s", gt, buf); + break; + case WHENSPEC_NOW: + if (strcmp("now", gt)) + die("Date in ident must be 'now': %s", buf); + datestamp(ident + name_len, 24); + break; + } + + return ident; +} + +static void cmd_new_blob(void) +{ + size_t l; + void *d; + + read_next_command(); + cmd_mark(); + d = cmd_data(&l); + + if (store_object(OBJ_BLOB, d, l, &last_blob, NULL, next_mark)) + free(d); +} + +static void unload_one_branch(void) +{ + while (cur_active_branches + && cur_active_branches >= max_active_branches) { + unsigned long min_commit = ULONG_MAX; + struct branch *e, *l = NULL, *p = NULL; + + for (e = active_branches; e; e = e->active_next_branch) { + if (e->last_commit < min_commit) { + p = l; + min_commit = e->last_commit; + } + l = e; + } + + if (p) { + e = p->active_next_branch; + p->active_next_branch = e->active_next_branch; + } else { + e = active_branches; + active_branches = e->active_next_branch; + } + e->active_next_branch = NULL; + if (e->branch_tree.tree) { + release_tree_content_recursive(e->branch_tree.tree); + e->branch_tree.tree = NULL; + } + cur_active_branches--; + } +} + +static void load_branch(struct branch *b) +{ + load_tree(&b->branch_tree); + b->active_next_branch = active_branches; + active_branches = b; + cur_active_branches++; + branch_load_count++; +} + +static void file_change_m(struct branch *b) +{ + const char *p = command_buf.buf + 2; + char *p_uq; + const char *endp; + struct object_entry *oe = oe; + unsigned char sha1[20]; + uint16_t mode, inline_data = 0; + char type[20]; + + p = get_mode(p, &mode); + if (!p) + die("Corrupt mode: %s", command_buf.buf); + switch (mode) { + case S_IFREG | 0644: + case S_IFREG | 0755: + case S_IFLNK: + case 0644: + case 0755: + /* ok */ + break; + default: + die("Corrupt mode: %s", command_buf.buf); + } + + if (*p == ':') { + char *x; + oe = find_mark(strtoumax(p + 1, &x, 10)); + hashcpy(sha1, oe->sha1); + p = x; + } else if (!strncmp("inline", p, 6)) { + inline_data = 1; + p += 6; + } else { + if (get_sha1_hex(p, sha1)) + die("Invalid SHA1: %s", command_buf.buf); + oe = find_object(sha1); + p += 40; + } + if (*p++ != ' ') + die("Missing space after SHA1: %s", command_buf.buf); + + p_uq = unquote_c_style(p, &endp); + if (p_uq) { + if (*endp) + die("Garbage after path in: %s", command_buf.buf); + p = p_uq; + } + + if (inline_data) { + size_t l; + void *d; + if (!p_uq) + p = p_uq = xstrdup(p); + read_next_command(); + d = cmd_data(&l); + if (store_object(OBJ_BLOB, d, l, &last_blob, sha1, 0)) + free(d); + } else if (oe) { + if (oe->type != OBJ_BLOB) + die("Not a blob (actually a %s): %s", + command_buf.buf, type_names[oe->type]); + } else { + if (sha1_object_info(sha1, type, NULL)) + die("Blob not found: %s", command_buf.buf); + if (strcmp(blob_type, type)) + die("Not a blob (actually a %s): %s", + command_buf.buf, type); + } + + tree_content_set(&b->branch_tree, p, sha1, S_IFREG | mode); + free(p_uq); +} + +static void file_change_d(struct branch *b) +{ + const char *p = command_buf.buf + 2; + char *p_uq; + const char *endp; + + p_uq = unquote_c_style(p, &endp); + if (p_uq) { + if (*endp) + die("Garbage after path in: %s", command_buf.buf); + p = p_uq; + } + tree_content_remove(&b->branch_tree, p); + free(p_uq); +} + +static void cmd_from(struct branch *b) +{ + const char *from; + struct branch *s; + + if (strncmp("from ", command_buf.buf, 5)) + return; + + if (b->last_commit) + die("Can't reinitailize branch %s", b->name); + + from = strchr(command_buf.buf, ' ') + 1; + s = lookup_branch(from); + if (b == s) + die("Can't create a branch from itself: %s", b->name); + else if (s) { + unsigned char *t = s->branch_tree.versions[1].sha1; + hashcpy(b->sha1, s->sha1); + hashcpy(b->branch_tree.versions[0].sha1, t); + hashcpy(b->branch_tree.versions[1].sha1, t); + } else if (*from == ':') { + uintmax_t idnum = strtoumax(from + 1, NULL, 10); + struct object_entry *oe = find_mark(idnum); + unsigned long size; + char *buf; + if (oe->type != OBJ_COMMIT) + die("Mark :%ju not a commit", idnum); + hashcpy(b->sha1, oe->sha1); + buf = gfi_unpack_entry(oe, &size); + if (!buf || size < 46) + die("Not a valid commit: %s", from); + if (memcmp("tree ", buf, 5) + || get_sha1_hex(buf + 5, b->branch_tree.versions[1].sha1)) + die("The commit %s is corrupt", sha1_to_hex(b->sha1)); + free(buf); + hashcpy(b->branch_tree.versions[0].sha1, + b->branch_tree.versions[1].sha1); + } else if (!get_sha1(from, b->sha1)) { + if (is_null_sha1(b->sha1)) { + hashclr(b->branch_tree.versions[0].sha1); + hashclr(b->branch_tree.versions[1].sha1); + } else { + unsigned long size; + char *buf; + + buf = read_object_with_reference(b->sha1, + type_names[OBJ_COMMIT], &size, b->sha1); + if (!buf || size < 46) + die("Not a valid commit: %s", from); + if (memcmp("tree ", buf, 5) + || get_sha1_hex(buf + 5, b->branch_tree.versions[1].sha1)) + die("The commit %s is corrupt", sha1_to_hex(b->sha1)); + free(buf); + hashcpy(b->branch_tree.versions[0].sha1, + b->branch_tree.versions[1].sha1); + } + } else + die("Invalid ref name or SHA1 expression: %s", from); + + read_next_command(); +} + +static struct hash_list *cmd_merge(unsigned int *count) +{ + struct hash_list *list = NULL, *n, *e = e; + const char *from; + struct branch *s; + + *count = 0; + while (!strncmp("merge ", command_buf.buf, 6)) { + from = strchr(command_buf.buf, ' ') + 1; + n = xmalloc(sizeof(*n)); + s = lookup_branch(from); + if (s) + hashcpy(n->sha1, s->sha1); + else if (*from == ':') { + uintmax_t idnum = strtoumax(from + 1, NULL, 10); + struct object_entry *oe = find_mark(idnum); + if (oe->type != OBJ_COMMIT) + die("Mark :%ju not a commit", idnum); + hashcpy(n->sha1, oe->sha1); + } else if (get_sha1(from, n->sha1)) + die("Invalid ref name or SHA1 expression: %s", from); + + n->next = NULL; + if (list) + e->next = n; + else + list = n; + e = n; + (*count)++; + read_next_command(); + } + return list; +} + +static void cmd_new_commit(void) +{ + struct branch *b; + void *msg; + size_t msglen; + char *sp; + char *author = NULL; + char *committer = NULL; + struct hash_list *merge_list = NULL; + unsigned int merge_count; + + /* Obtain the branch name from the rest of our command */ + sp = strchr(command_buf.buf, ' ') + 1; + b = lookup_branch(sp); + if (!b) + b = new_branch(sp); + + read_next_command(); + cmd_mark(); + if (!strncmp("author ", command_buf.buf, 7)) { + author = parse_ident(command_buf.buf + 7); + read_next_command(); + } + if (!strncmp("committer ", command_buf.buf, 10)) { + committer = parse_ident(command_buf.buf + 10); + read_next_command(); + } + if (!committer) + die("Expected committer but didn't get one"); + msg = cmd_data(&msglen); + read_next_command(); + cmd_from(b); + merge_list = cmd_merge(&merge_count); + + /* ensure the branch is active/loaded */ + if (!b->branch_tree.tree || !max_active_branches) { + unload_one_branch(); + load_branch(b); + } + + /* file_change* */ + for (;;) { + if (1 == command_buf.len) + break; + else if (!strncmp("M ", command_buf.buf, 2)) + file_change_m(b); + else if (!strncmp("D ", command_buf.buf, 2)) + file_change_d(b); + else + die("Unsupported file_change: %s", command_buf.buf); + read_next_command(); + } + + /* build the tree and the commit */ + store_tree(&b->branch_tree); + hashcpy(b->branch_tree.versions[0].sha1, + b->branch_tree.versions[1].sha1); + size_dbuf(&new_data, 114 + msglen + + merge_count * 49 + + (author + ? strlen(author) + strlen(committer) + : 2 * strlen(committer))); + sp = new_data.buffer; + sp += sprintf(sp, "tree %s\n", + sha1_to_hex(b->branch_tree.versions[1].sha1)); + if (!is_null_sha1(b->sha1)) + sp += sprintf(sp, "parent %s\n", sha1_to_hex(b->sha1)); + while (merge_list) { + struct hash_list *next = merge_list->next; + sp += sprintf(sp, "parent %s\n", sha1_to_hex(merge_list->sha1)); + free(merge_list); + merge_list = next; + } + sp += sprintf(sp, "author %s\n", author ? author : committer); + sp += sprintf(sp, "committer %s\n", committer); + *sp++ = '\n'; + memcpy(sp, msg, msglen); + sp += msglen; + free(author); + free(committer); + free(msg); + + if (!store_object(OBJ_COMMIT, + new_data.buffer, sp - (char*)new_data.buffer, + NULL, b->sha1, next_mark)) + b->pack_id = pack_id; + b->last_commit = object_count_by_type[OBJ_COMMIT]; +} + +static void cmd_new_tag(void) +{ + char *sp; + const char *from; + char *tagger; + struct branch *s; + void *msg; + size_t msglen; + struct tag *t; + uintmax_t from_mark = 0; + unsigned char sha1[20]; + + /* Obtain the new tag name from the rest of our command */ + sp = strchr(command_buf.buf, ' ') + 1; + t = pool_alloc(sizeof(struct tag)); + t->next_tag = NULL; + t->name = pool_strdup(sp); + if (last_tag) + last_tag->next_tag = t; + else + first_tag = t; + last_tag = t; + read_next_command(); + + /* from ... */ + if (strncmp("from ", command_buf.buf, 5)) + die("Expected from command, got %s", command_buf.buf); + from = strchr(command_buf.buf, ' ') + 1; + s = lookup_branch(from); + if (s) { + hashcpy(sha1, s->sha1); + } else if (*from == ':') { + struct object_entry *oe; + from_mark = strtoumax(from + 1, NULL, 10); + oe = find_mark(from_mark); + if (oe->type != OBJ_COMMIT) + die("Mark :%ju not a commit", from_mark); + hashcpy(sha1, oe->sha1); + } else if (!get_sha1(from, sha1)) { + unsigned long size; + char *buf; + + buf = read_object_with_reference(sha1, + type_names[OBJ_COMMIT], &size, sha1); + if (!buf || size < 46) + die("Not a valid commit: %s", from); + free(buf); + } else + die("Invalid ref name or SHA1 expression: %s", from); + read_next_command(); + + /* tagger ... */ + if (strncmp("tagger ", command_buf.buf, 7)) + die("Expected tagger command, got %s", command_buf.buf); + tagger = parse_ident(command_buf.buf + 7); + + /* tag payload/message */ + read_next_command(); + msg = cmd_data(&msglen); + + /* build the tag object */ + size_dbuf(&new_data, 67+strlen(t->name)+strlen(tagger)+msglen); + sp = new_data.buffer; + sp += sprintf(sp, "object %s\n", sha1_to_hex(sha1)); + sp += sprintf(sp, "type %s\n", type_names[OBJ_COMMIT]); + sp += sprintf(sp, "tag %s\n", t->name); + sp += sprintf(sp, "tagger %s\n", tagger); + *sp++ = '\n'; + memcpy(sp, msg, msglen); + sp += msglen; + free(tagger); + free(msg); + + if (store_object(OBJ_TAG, new_data.buffer, + sp - (char*)new_data.buffer, + NULL, t->sha1, 0)) + t->pack_id = MAX_PACK_ID; + else + t->pack_id = pack_id; +} + +static void cmd_reset_branch(void) +{ + struct branch *b; + char *sp; + + /* Obtain the branch name from the rest of our command */ + sp = strchr(command_buf.buf, ' ') + 1; + b = lookup_branch(sp); + if (b) { + b->last_commit = 0; + if (b->branch_tree.tree) { + release_tree_content_recursive(b->branch_tree.tree); + b->branch_tree.tree = NULL; + } + } + else + b = new_branch(sp); + read_next_command(); + cmd_from(b); +} + +static void cmd_checkpoint(void) +{ + if (object_count) + checkpoint(); + read_next_command(); +} + +static const char fast_import_usage[] = +"git-fast-import [--date-format=f] [--max-pack-size=n] [--depth=n] [--active-branches=n] [--export-marks=marks.file]"; + +int main(int argc, const char **argv) +{ + int i; + uintmax_t total_count, duplicate_count; + + git_config(git_default_config); + + for (i = 1; i < argc; i++) { + const char *a = argv[i]; + + if (*a != '-' || !strcmp(a, "--")) + break; + else if (!strncmp(a, "--date-format=", 14)) { + const char *fmt = a + 14; + if (!strcmp(fmt, "raw")) + whenspec = WHENSPEC_RAW; + else if (!strcmp(fmt, "rfc2822")) + whenspec = WHENSPEC_RFC2822; + else if (!strcmp(fmt, "now")) + whenspec = WHENSPEC_NOW; + else + die("unknown --date-format argument %s", fmt); + } + else if (!strncmp(a, "--max-pack-size=", 16)) + max_packsize = strtoumax(a + 16, NULL, 0) * 1024 * 1024; + else if (!strncmp(a, "--depth=", 8)) + max_depth = strtoul(a + 8, NULL, 0); + else if (!strncmp(a, "--active-branches=", 18)) + max_active_branches = strtoul(a + 18, NULL, 0); + else if (!strncmp(a, "--export-marks=", 15)) + mark_file = a + 15; + else if (!strcmp(a, "--force")) + force_update = 1; + else + die("unknown option %s", a); + } + if (i != argc) + usage(fast_import_usage); + + alloc_objects(object_entry_alloc); + strbuf_init(&command_buf); + + atom_table = xcalloc(atom_table_sz, sizeof(struct atom_str*)); + branch_table = xcalloc(branch_table_sz, sizeof(struct branch*)); + avail_tree_table = xcalloc(avail_tree_table_sz, sizeof(struct avail_tree_content*)); + marks = pool_calloc(1, sizeof(struct mark_set)); + + start_packfile(); + for (;;) { + read_next_command(); + if (command_buf.eof) + break; + else if (!strcmp("blob", command_buf.buf)) + cmd_new_blob(); + else if (!strncmp("commit ", command_buf.buf, 7)) + cmd_new_commit(); + else if (!strncmp("tag ", command_buf.buf, 4)) + cmd_new_tag(); + else if (!strncmp("reset ", command_buf.buf, 6)) + cmd_reset_branch(); + else if (!strcmp("checkpoint", command_buf.buf)) + cmd_checkpoint(); + else + die("Unsupported command: %s", command_buf.buf); + } + end_packfile(); + + dump_branches(); + dump_tags(); + unkeep_all_packs(); + dump_marks(); + + total_count = 0; + for (i = 0; i < ARRAY_SIZE(object_count_by_type); i++) + total_count += object_count_by_type[i]; + duplicate_count = 0; + for (i = 0; i < ARRAY_SIZE(duplicate_count_by_type); i++) + duplicate_count += duplicate_count_by_type[i]; + + fprintf(stderr, "%s statistics:\n", argv[0]); + fprintf(stderr, "---------------------------------------------------------------------\n"); + fprintf(stderr, "Alloc'd objects: %10ju\n", alloc_count); + fprintf(stderr, "Total objects: %10ju (%10ju duplicates )\n", total_count, duplicate_count); + fprintf(stderr, " blobs : %10ju (%10ju duplicates %10ju deltas)\n", object_count_by_type[OBJ_BLOB], duplicate_count_by_type[OBJ_BLOB], delta_count_by_type[OBJ_BLOB]); + fprintf(stderr, " trees : %10ju (%10ju duplicates %10ju deltas)\n", object_count_by_type[OBJ_TREE], duplicate_count_by_type[OBJ_TREE], delta_count_by_type[OBJ_TREE]); + fprintf(stderr, " commits: %10ju (%10ju duplicates %10ju deltas)\n", object_count_by_type[OBJ_COMMIT], duplicate_count_by_type[OBJ_COMMIT], delta_count_by_type[OBJ_COMMIT]); + fprintf(stderr, " tags : %10ju (%10ju duplicates %10ju deltas)\n", object_count_by_type[OBJ_TAG], duplicate_count_by_type[OBJ_TAG], delta_count_by_type[OBJ_TAG]); + fprintf(stderr, "Total branches: %10lu (%10lu loads )\n", branch_count, branch_load_count); + fprintf(stderr, " marks: %10ju (%10ju unique )\n", (((uintmax_t)1) << marks->shift) * 1024, marks_set_count); + fprintf(stderr, " atoms: %10u\n", atom_cnt); + fprintf(stderr, "Memory total: %10ju KiB\n", (total_allocd + alloc_count*sizeof(struct object_entry))/1024); + fprintf(stderr, " pools: %10lu KiB\n", total_allocd/1024); + fprintf(stderr, " objects: %10ju KiB\n", (alloc_count*sizeof(struct object_entry))/1024); + fprintf(stderr, "---------------------------------------------------------------------\n"); + pack_report(); + fprintf(stderr, "---------------------------------------------------------------------\n"); + fprintf(stderr, "\n"); + + return failure ? 1 : 0; +} diff --git a/t/t9300-fast-import.sh b/t/t9300-fast-import.sh new file mode 100755 index 0000000000..23a2ba78f6 --- /dev/null +++ b/t/t9300-fast-import.sh @@ -0,0 +1,359 @@ +#!/bin/sh +# +# Copyright (c) 2007 Shawn Pearce +# + +test_description='test git-fast-import utility' +. ./test-lib.sh +. ../diff-lib.sh ;# test-lib chdir's into trash + +file2_data='file2 +second line of EOF' + +file3_data='EOF +in 3rd file + END' + +file4_data=abcd +file4_len=4 + +file5_data='an inline file. + we should see it later.' + +file6_data='#!/bin/sh +echo "$@"' + +### +### series A +### + +test_tick +cat >input <<INPUT_END +blob +mark :2 +data <<EOF +$file2_data +EOF + +blob +mark :3 +data <<END +$file3_data +END + +blob +mark :4 +data $file4_len +$file4_data +commit refs/heads/master +mark :5 +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +initial +COMMIT + +M 644 :2 file2 +M 644 :3 file3 +M 755 :4 file4 + +INPUT_END +test_expect_success \ + 'A: create pack from stdin' \ + 'git-fast-import --export-marks=marks.out <input && + git-whatchanged master' +test_expect_success \ + 'A: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' + +cat >expect <<EOF +author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + +initial +EOF +test_expect_success \ + 'A: verify commit' \ + 'git-cat-file commit master | sed 1d >actual && + diff -u expect actual' + +cat >expect <<EOF +100644 blob file2 +100644 blob file3 +100755 blob file4 +EOF +test_expect_success \ + 'A: verify tree' \ + 'git-cat-file -p master^{tree} | sed "s/ [0-9a-f]* / /" >actual && + diff -u expect actual' + +echo "$file2_data" >expect +test_expect_success \ + 'A: verify file2' \ + 'git-cat-file blob master:file2 >actual && diff -u expect actual' + +echo "$file3_data" >expect +test_expect_success \ + 'A: verify file3' \ + 'git-cat-file blob master:file3 >actual && diff -u expect actual' + +printf "$file4_data" >expect +test_expect_success \ + 'A: verify file4' \ + 'git-cat-file blob master:file4 >actual && diff -u expect actual' + +cat >expect <<EOF +:2 `git-rev-parse --verify master:file2` +:3 `git-rev-parse --verify master:file3` +:4 `git-rev-parse --verify master:file4` +:5 `git-rev-parse --verify master^0` +EOF +test_expect_success \ + 'A: verify marks output' \ + 'diff -u expect marks.out' + +### +### series B +### + +test_tick +cat >input <<INPUT_END +commit refs/heads/branch +mark :1 +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +corrupt +COMMIT + +from refs/heads/master +M 755 0000000000000000000000000000000000000001 zero1 + +INPUT_END +test_expect_failure \ + 'B: fail on invalid blob sha1' \ + 'git-fast-import <input' +rm -f .git/objects/pack_* .git/objects/index_* + +### +### series C +### + +newf=`echo hi newf | git-hash-object -w --stdin` +oldf=`git-rev-parse --verify master:file2` +test_tick +cat >input <<INPUT_END +commit refs/heads/branch +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +second +COMMIT + +from refs/heads/master +M 644 $oldf file2/oldf +M 755 $newf file2/newf +D file3 + +INPUT_END +test_expect_success \ + 'C: incremental import create pack from stdin' \ + 'git-fast-import <input && + git-whatchanged branch' +test_expect_success \ + 'C: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' +test_expect_success \ + 'C: validate reuse existing blob' \ + 'test $newf = `git-rev-parse --verify branch:file2/newf` + test $oldf = `git-rev-parse --verify branch:file2/oldf`' + +cat >expect <<EOF +parent `git-rev-parse --verify master^0` +author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + +second +EOF +test_expect_success \ + 'C: verify commit' \ + 'git-cat-file commit branch | sed 1d >actual && + diff -u expect actual' + +cat >expect <<EOF +:000000 100755 0000000000000000000000000000000000000000 f1fb5da718392694d0076d677d6d0e364c79b0bc A file2/newf +:100644 100644 7123f7f44e39be127c5eb701e5968176ee9d78b1 7123f7f44e39be127c5eb701e5968176ee9d78b1 R100 file2 file2/oldf +:100644 000000 0d92e9f3374ae2947c23aa477cbc68ce598135f1 0000000000000000000000000000000000000000 D file3 +EOF +git-diff-tree -M -r master branch >actual +test_expect_success \ + 'C: validate rename result' \ + 'compare_diff_raw expect actual' + +### +### series D +### + +test_tick +cat >input <<INPUT_END +commit refs/heads/branch +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +third +COMMIT + +from refs/heads/branch^0 +M 644 inline newdir/interesting +data <<EOF +$file5_data +EOF + +M 755 inline newdir/exec.sh +data <<EOF +$file6_data +EOF + +INPUT_END +test_expect_success \ + 'D: inline data in commit' \ + 'git-fast-import <input && + git-whatchanged branch' +test_expect_success \ + 'D: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' + +cat >expect <<EOF +:000000 100755 0000000000000000000000000000000000000000 35a59026a33beac1569b1c7f66f3090ce9c09afc A newdir/exec.sh +:000000 100644 0000000000000000000000000000000000000000 046d0371e9220107917db0d0e030628de8a1de9b A newdir/interesting +EOF +git-diff-tree -M -r branch^ branch >actual +test_expect_success \ + 'D: validate new files added' \ + 'compare_diff_raw expect actual' + +echo "$file5_data" >expect +test_expect_success \ + 'D: verify file5' \ + 'git-cat-file blob branch:newdir/interesting >actual && + diff -u expect actual' + +echo "$file6_data" >expect +test_expect_success \ + 'D: verify file6' \ + 'git-cat-file blob branch:newdir/exec.sh >actual && + diff -u expect actual' + +### +### series E +### + +cat >input <<INPUT_END +commit refs/heads/branch +author $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> Tue Feb 6 11:22:18 2007 -0500 +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> Tue Feb 6 12:35:02 2007 -0500 +data <<COMMIT +RFC 2822 type date +COMMIT + +from refs/heads/branch^0 + +INPUT_END +test_expect_failure \ + 'E: rfc2822 date, --date-format=raw' \ + 'git-fast-import --date-format=raw <input' +test_expect_success \ + 'E: rfc2822 date, --date-format=rfc2822' \ + 'git-fast-import --date-format=rfc2822 <input' +test_expect_success \ + 'E: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' + +cat >expect <<EOF +author $GIT_AUTHOR_NAME <$GIT_AUTHOR_EMAIL> 1170778938 -0500 +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> 1170783302 -0500 + +RFC 2822 type date +EOF +test_expect_success \ + 'E: verify commit' \ + 'git-cat-file commit branch | sed 1,2d >actual && + diff -u expect actual' + +### +### series F +### + +old_branch=`git-rev-parse --verify branch^0` +test_tick +cat >input <<INPUT_END +commit refs/heads/branch +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +losing things already? +COMMIT + +from refs/heads/branch~1 + +reset refs/heads/other +from refs/heads/branch + +INPUT_END +test_expect_success \ + 'F: non-fast-forward update skips' \ + 'if git-fast-import <input + then + echo BAD gfi did not fail + return 1 + else + if test $old_branch = `git-rev-parse --verify branch^0` + then + : branch unaffected and failure returned + return 0 + else + echo BAD gfi changed branch $old_branch + return 1 + fi + fi + ' +test_expect_success \ + 'F: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' + +cat >expect <<EOF +tree `git-rev-parse branch~1^{tree}` +parent `git-rev-parse branch~1` +author $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE + +losing things already? +EOF +test_expect_success \ + 'F: verify other commit' \ + 'git-cat-file commit other >actual && + diff -u expect actual' + +### +### series G +### + +old_branch=`git-rev-parse --verify branch^0` +test_tick +cat >input <<INPUT_END +commit refs/heads/branch +committer $GIT_COMMITTER_NAME <$GIT_COMMITTER_EMAIL> $GIT_COMMITTER_DATE +data <<COMMIT +losing things already? +COMMIT + +from refs/heads/branch~1 + +INPUT_END +test_expect_success \ + 'G: non-fast-forward update forced' \ + 'git-fast-import --force <input' +test_expect_success \ + 'G: verify pack' \ + 'for p in .git/objects/pack/*.pack;do git-verify-pack $p||exit;done' +test_expect_success \ + 'G: branch changed, but logged' \ + 'test $old_branch != `git-rev-parse --verify branch^0` && + test $old_branch = `git-rev-parse --verify branch@{1}`' + +test_done |