Git的基本概念与入门

基本概念

在本章中,我们将介绍一个分布式版本控制系统的设计思路,以及它与集中式版本控制系统的不同之处。除此之外,我们还将带你了解分布式版本库的具体工作方式,以及为什么我们会说,在Git中创建分支和合并分支不是个大不了的问题。

1 分布式版本控制,有何过人之处

在具体探讨分布式版本控制的概念之前,让我们先来快速回顾一下传统的集中式版本控制架构。

图1中所显示的就是一个集中式版本控制系统(例如CVS或Subversion)的典型布局。每个开发者都在他或她自己的计算机上有一个包含所有项目文件的工作目录(即工作区)。当该开发者在本地做了修改之后,他或她就会定期将修改提交给某台中央服务器。然后,开发者在执行更新操作的同时也会从该服务器上捡取出其他开发者所做的修改。这台中央服务器上存储着这些文件(即版本库)的当前版本和历史版本。因此,这些被并行开发的分支,以及各种被命名(标记)的版本都将会被集中管理。

图1 集中式版本控制

而在分布式版本控制系统(见图2)中,开发者环境与服务器环境之间是没有分隔的。每一个开发者都同时拥有一个用于当前文件操作的工作区与一个用于存储该项目所有版本、分支以及标签的本地版本库(我们称其为一份克隆)。每个开发者的修改都会被载入成一次次的新版本提交(commit), 首先提交到其本地版本库中。然后,其他开发者就会立即看到新的版本。通过推送(push)和拉回(pull)命令,我们可以将这些修改从一个版本库传送到另一个版本库中。这样一来,从技术上来看,这里所有的版本库在分布式架构上的地位是同等的。因此从理论上来讲,我们不再需要借助服务器,就可以将某一台开发工作机上所做的所有修改直接传送给另一开发工作机。当然在具体实践中,Git中的服务器版本库也扮演了重要的角色,例如以下这些特型版本库。

图2 分布式版本控制

项目版本库(blessed repository):该版本库主要用于存储由“官方”创建并发行的版本。

共享版本库(shared repository):该版本库主要用于开发团队内人员之间的文件交换。在小型项目中,项目版本库本身就可以胜任这一角色了。但在多点开发的条件下,我们可能就会需要几个这样的专用版本库。

工作流版本库(workflow repository):工作流版本库通常只用于填充那些代表工作流中某种特定进展状态的修改,例如审核通过后的状态等。

派生版本库(fork repository):该版本库主要用于从开发主线分离出某部分内容(例如,分离出那些开发耗时较长,不适合在一个普通发布周期中完成的内容),或者隔离出可能永远不会被包含在主线中的、用于实验的那部分开发进展。

下面,我们再来看看分布式系统相对于集中式的优点有哪些。

高性能:几乎所有的操作都无需进行网络访问,均可直接在本地执行。

高效的工作方式:开发者可通过多个本地分支在不同任务之间进行快速切换。

离线功能:开发者可以在没有服务器连接的情况下执行提交、创建分支、版本标签等操作。之后再将其上传服务器。

灵活的开发进程:我们可以在团队和公司中为其他部门建立专用的版本库,例如为方便与测试人员交流而建的版本库。这样相关修改就很容易发布,因为只是特定版本库上的一次推送。

备份作用:由于每个开发者都持有一份拥有完整历史版本的版本库副本,所以因服务器故障而导致数据丢失的可能性是微乎其微的。

可维护性:对于那些难以对付的重构工作,我们可以在将成功传送给其原始版本库之前,先在该版本库的副本上尝试一下。

2 版本库,分布式工作的基础所在

其实,版本库本质上就是一个高效的数据存储结构而已,由以下部分组成。

文件(即blob):这里既包含了文本也包含了二进制数据,这些数据将不以文件名的形式被保存。

目录(即Tree):目录中保存的是与文件名相关联的内容,其中也会包含其他目录。

版本(即commit):每一个版本所定义的都是相应目录的某个可恢复的状态。每当我们创建一个新的版本时,其作者、时间、注释以及其之前的版本都将会被保存下来。

对于所有的数据,它们都会被计算成一个十六进制散列值(例如像1632acb65b01 c6b621d6e1105205773931bb1a41这样的值)。这个散列值将会被用作相关对象的引用,以及日后恢复数据时所需的键值(见图3)。

图3 版本库中的对象存储

也就是说,一个提交对象的散列值实际上就是它的“版本号”,如果我们持有某一提交的散列值,就可以用它来检查对应版本是否存在于某一版本库中。如果存在,我们就可以将其恢复到当前工作区相应的目录中。如果该版本不存在,我们也可以从其他版本库中单独导入(拉回)该提交所引用的全部对象。

接下来,我们来看看采用这种散列值和这种既定的版本库结构究竟有哪些优势。

高性能:通过散列值来访问数据是非常快的。

冗余度——释放存储空间:相同的文件内容只需存储一次即可。

分布式版本号:由于相关散列值是根据文件,作者和日期来计算的,所以版本也可以“离线”产生,不用担心将来会因此而发生版本冲突。

版本库间的高效同步:当我们将某一提交从一个版本库传递给另一个版本库时,只需要传送那些目标版本库中不存在的对象即可。而正是因为有了散列值的帮助,我们才能很快地判断相关对象是否已经存在。

数据完整性:由于散列值是根据数据的内容来计算的,所以我们可以随时通过Git来查看某一散列值是否与相关数据匹配。以检测该数据上可能的意外变化或恶意操作。

自动重命名检测:被重命名的文件可以被自动检测到,因为根据该文件内容计算出的散列值并没有发生变化。也正因为如此,Git中并没有专用的重命名命令,只需移动命令即可。

3 分支的创建与合并很简单

对于大多数版本控制系统来说,分支的创建与合并通常会因其特殊性而被认为是高级拓展操作。但由于Git最初就是为了方便那些散落在世界各地的Linux内核开发者而创建的,合并多方努力的结果一直都是其面临的最大挑战之一,所以Git的设计目标之一就是要让分支的创建与合并操作变得尽可能地简单且安全。

在下面的图4中,我们向你展示了开发者是如何通过创建分支的方式来进行并行开发的。图中的每一个点都代表了该项目的一个版本(即commit)。而由于在Git中,我们只能对整个项目进行版本化,所以每个点同时也代表了属于同一版本的各个文件。

图4 因开发者的并行开发而出现的分支创建操作

如上所示,图中两位开发者的起点是同一个版本。之后两人各自做了修改,并提交了修改。这时候,对于这两位开发者各自的版本库来说,该项目已经有了两个不同的版本。也就是说,他们在这里创建了两个分支。接下来,如果其中一个开发者想要导入另一个人的修改,他/她就可以用Git来进行版本合并。如果合并成功了,Git就会创建一个合并提交,其中会包含两位开发者所做的修改。这时如果另一位开发者也取回了这一提交,两位开发者的项目就又回到了同一个版本。

在上面的例子中,分支的创建是非计划性的,其原因仅仅是两个开发者在并行开发同一个软件罢了。在Git中,我们当然也可以开启有针对性的分支,即显式地创建一个分支(见图5)。显式分支通常主要用于协调某一种功能性的并行开发。

图5 针对不同任务的显式分支

版本库在执行拉回和推送操作时,可以具体指定其针对的是哪一些分支。当然,除了这些简单的分支创建和合并处理外,我们也可以对分支执行以下动作。

移植分支:我们可以直接将某一分支中的提交转移到另一个版本库中。

只传送特定修改:我们可以将某一分支中的某一次或某几次提交直接复制到另一个分支中。这就是所谓的捡取处理。

清理历史:我们可以对分支历史进行改造、排序和删除。这有利于为该项目建立更好的历史文档。我们称这种处理为交互式重订(interactive rebasing)。

4 本章小结

在阅读完本章之后,我们希望你现在基本上熟悉了Git中的这些基本概念。也就是说,即使你现在放下了这本书(当然,希望不会!),你也可以参加与分布式版本控制系统有关的讨论,阐述其中使用散列值的必要性和实用性,介绍Git中的分支创建与合并操作了。

当然,你可能还会有以下疑问。

我们应该如何利用这些基本概念来管理项目呢?

我们应该如何协调多个版本库呢?

我们究竟需要多少分支呢?

我们应该如何整合自己的构建服务器呢?

对于第一个问题,你可以继续阅读下一章内容。在下一章中,你将会看到那些具体用于创建版本库、版本以及版本库之间更替提交的命令。至于其他问题,你也可以参考详细介绍工作流的那些章节。

另外,如果你是一个繁忙的项目管理者,还在犹豫不决是否要采用Git。我们会建议你再看看关于Git的局限性的的讨论,请参见第26章。

入门

如果你想试着用一下Git的话,那么我们马上就可以开始了。本章将会带领你创建自己的第一个项目。我们会为你演示那些用于提交修改版本、查看历史和与其他开发者交换版本的命令。

1 准备Git环境

首先,我们需要安装好Git。你可以在Git的官网上找到你所需要的一切:

http://git-scm.com/download

Git是一个高可配置软件。首先,我们可以宣布用config命令配置一下用户名和用户邮箱:[1]

> git config --global user.email "hans@mustermann.de"

2 第一个Git项目

在这里,我们建议你最好能为接下来的Git测试单独开辟一个项目。总之应先从一个简单的小项目开始。在我们这个小小的示例项目中,first-steps目录下只有两个文本文件,如图1所示。

图1 我们的示例项目

在开始摆弄这个玩具项目之前,我们建议你最好先做一个备份!尽管在Git中,想要造成永久性的删除或破坏也不是件容易的事情,而且每当你要做某些“危险”动作的时候,Git通常也会发出相应的警告消息。但是,有备无患总是好的。

1 创建版本库

现在,我们首先需要创建一个版本库,用于存储该项目本身及其历史。为此,我们需要在该项目目录中使用init命令。对于一个带版本库的项目目录,我们通常称之为工作区。

> cd /projects/first-steps

> git init

Initialized empty Git repository in /projects/first-steps/.git/

init命令会在上述目录中创建一个名为.git的隐藏目录,并在其中创建一个版本库。但请注意,该目录在Windows资源管理器或Mac Finder中可能是不可见的。

图2 本地版本库所在的目录

2 首次提交

接下来,我们需要将foo.txtbar.txt这两个文件添加到版本库中去。在Git中,我们通常将项目的一个版本称之为一次提交,但这要分两个步骤来实现。第一步,我们要先用add命令来确定哪些文件应被包含在下次提交中。第二步,再用commit命令将修改传送到版本库中,并赋予该提交一个散列值以便标识这次新提交。在这里,我们的散列值为2f43cd0,但可能会有所不同,因为该值取决于文件内容。

> git add foo.txt bar.txt

> git commit --message "Sample project imported."

master (root-commit) 2f43cd0] Sample project imported.

2 files changed, 2 insertions(+), 0 deletions(-)

create mode 100644 bar.txt

create mode 100644 foo.txt

3 检查状态

现在,我们来修改一下foo.txt文件的内容,先删除bar.txt文件,再添加一个名为bar.html的新文件。然后,status命令就会显示出该项目自上次提交以来所发生的所有修改。请注意,新文件bar.html在这里被标示成了未跟踪状态,这是因为我们还没有用add命令将其注册到版本库。

> git status

# On branch master

# Changed but not updated:

# (use "git add/rm ..." to update what will be committed)

# (use "git checkout -- ..." to discard changes in

#                                                    working directory)

#

#      deleted:  bar.txt

#      modified:  foo.txt

#

# Untracked files:

# (use "git add ..." to include in what will be committed)

#

#      bar.html

no changes added to commit (use "git add" and/or "git commit -a")

如果我们还想看到更多细节性的内容,也可以通过diff命令来显示其每个被修改的行。当然。有很多人可能会觉得diff的输出是个非常难读的东西。幸运的是,在这一领域,我们有许多工具和开发环境可用,它们可以将这一切显示得更为清晰(见图3)。

图3 图形工具(kdiff3)中的Diff报告

> git diff foo.txt

diff --git a/foo.txt b/foo.txt

index 191028.090387f 100644

--- a/foo.txt

+++ b/foo.txt

@@ -1 +1 @@

-foo

\ No newline at end of file

+foo foo

\ No newline at end of file

4 提交修改

接下来,所有的修改都必须要先被归档成一次新的提交。我们要对修改过的文件和新文件执行add命令,并对要删除的文件使用rm命令。

> git add foo.txt bar.html

> git rm bar.txt

rm 'bar.txt'

现在再次调用status命令,我们会看到所有的修改已经被纳入了下一次提交中。

> git status

# On branch master

# Changes to be committed:

#  (use "git reset HEAD ..." to unstage)

#

#      new file:  bar.html

#      deleted:    bar.txt

#      modified:  foo.txt

#

然后用commit命令提交这些修改。

> git commit --message "Some changes."

[master 7ac0f38] Some changes.

3 files changed, 2 insertions(+), 2 deletions(-)

create mode 100644 bar.html

delete mode 100644 bar.txt

5 显示历史

log命令可用来显示项目的历史,所有提交都会按时间顺序被降序排列出来。

> git log

commit 7ac0f38f575a60940ec93c98de11966d784e9e4f

Author: Rene Preissel

Date: Thu Dec 2 09:52:25 2010 +0100

Some changes.

commit 2f43cd047baadc1b52a8367b7cad2cb63bca05b7

Author: Rene Preissel

Date: Thu Dec 2 09:44:24 2010 +0100

Sample project imported.

3 Git的协作功能

现在,我们已经有了一个存放项目文件的工作区,以及一个存放项目历史的版本库。在一个像CVS和Subversion这样传统的集中式版本系统中,尽管每个开发者也都有属于他/她自己的工作区,但所有人都共享了一个通用的版本库。而在Git中,每个开发者拥有的是一个属于他/她自己的、自带独立版本库的工作区,因此这已经是一个不依赖于中央服务器的、完整的版本控制系统了。开发者们可以通过交换各自版本库中的提交来实现项目合作。下面我们就来做个试验,先创建一个新的工作区,以便我们模拟第二位开发者的活动。

3.1 克隆版本库

我们的这位新开发者首先要有一个属于他/她自己的版本库副本(也称为克隆体)。该副本中包含了所有的原始信息与整个项目的历史信息。下面。我们用clone命令来创建一个克隆体。

> git clone /projects/first-steps /projects/first-steps-clone

Cloning into first-steps-clone...

done.

现在,该项目结构如图4所示。

图4 样例项目与它的克隆体

3.2 从另一版本库中获取修改

下面,我们来修改一下first-steps/foo.txt文件,并执行以下操作来创建一次新提交。

> cd /projects/first-steps

> git add foo.txt

> git commit --message "A change in the original."

现在,新的提交已经被存入了我们原来的first-steps版本库中,但其克隆版本库(first-stepsclone)中依然缺失这次提交。为了让你更好地理解这一情况,我们来看一下first-steps的日志。

> git log --oneline

a662055 A change in the original.

7ac0f38 Some changes.

2f43cd0 Sample project imported.

在接下来的步骤中,我们再来修改克隆版本库中的first-steps-clone/bar.html文件,并执行以下操作。

> cd /projects/first-steps-clone

> git add bar.html

> git commit --message "A change in the clone."

> git log --oneline

1fcc06a A change in the clone.

7ac0f38 Some changes.

2f43cd0 Sample project imported.

现在,我们在两个版本库中各做了一次新的提交。接下来,我们要用pull命令将原版本库中的新提交传递给它的克隆体。由于之前我们在创建克隆版本库时,原版本库的路径就已经被存储在了它的克隆体中,因此pull命令知道该从哪里去取回新的提交。

> cd /projects/first-steps-clone

> git pull

remote: Counting objects: 5, done.

remote: Compressing objects: 100% (2/2), done.

remote: Total 3 (delta 0), reused 0 (delta 0)

Unpacking objects: 100% (3/3), done.

From /projects/first-steps

7ac0f38..a662055 master -> origin/master

Merge made by recursive.

foo.txt |      2 +-

1 files changed, 1 insertions(+), 1 deletions(-)

如上所示,pull命令从原版本库中取回了新的修改,将它们与克隆体中的本地修改进行了对比,并在工作区中合并了两边的修改,创建了一次新的提交。这个过程就是所谓的合并(merge)。

请注意!合并过程在某些情况下可能会带来冲突。一旦遇到了这种情况,Git中就不能进行自动化的版本合并了。在这种情况下,我们就必须要手动清理一些文件,然后再确认要提交哪些修改。

在拉回(pull)、合并(merge)的过程完成之后,我们可以用一个新的log命令来查看结果。这次是日志的图形化版本。

> git log --graph

9e7d7b9 Merge branch ’master’ of /projects/first-steps

*

|\

| * a662055 A change in the original.

* | 1fcc06a A change in the clone.

|/

* 7ac0f38 Some changes.

* 2f43cd0 Sample project imported.

这一次,历史记录不再是一条直线了。在上面的日志中,我们可以很清晰地看到并行开发的过程(即中间的两次提交),以及之后用于合并分支的那次合并提交(即顶部的那次提交)。

3.3 从任意版本库中取回修改

在没有参数的情况下,pull命令只在克隆版本库中能发挥作用,因为只有该克隆体中有默认的原版本库的连接。当我们执行pull操作时,也可以用参数来指定任意版本库的路径,以便从某一特定开发分支中提取相关修改。

现在,让我们将克隆体中的修改pull到原版本库中吧。

> cd /projects/first-steps

> git pull /projects/first-steps-clone master

remote: Counting objects: 8, done.

remote: Compressing objects: 100% (4/4), done.

remote: Total 5 (delta 0), reused 0 (delta 0)

Unpacking objects: 100% (5/5), done.

From /projects/first-steps-clone

* branch          master →  FETCH_HEAD

Updating a662055..9e7d7b9

Fast-forward

bar.html |    2 +-

1 files changed, 1 insertions(+), 1 deletions(-)

3.4 创建共享版本库

除了可以用pull命令从其他版本库中取回相关提交外,我们也可以用push命令将提交传送给其他版本库。只不过,push命令只适用于那些没有开发者在上面开展具体工作的版本库。最好的方法就是创建一个不带工作区的版本库,我们称之为裸版本库(bare repository)。你可以使用clone命令的--bare选项来创建一个裸版本库。裸版本库通常可被用来充当开发者们传递提交(使用push命令)的汇聚点,以便其他人可以从中拉回他们所做的修改。下面我们来看一个裸版本库(见图5)。

图5 裸版本库(一个没有工作区的版本库)

> git clone --bare /projects/first-steps

/projects/first-steps-bare.git

Cloning into bare repository first-steps-bare.git...

done.

3.5 用push命令上载修改

为了演示push命令的使用,我们需要再次修改一下firststeps/foo.txt文件,并执行以下操作来创建一次新的提交。

> cd /projects/first-steps

> git add foo.txt

> git commit --message "More changes in the original."

接下来,我们就可以用push命令向共享版本库传送该提交了(见图6)。该指令的参数要求与pull命令相同,我们需要指定目标版本库的路径及其分支。

> git push /projects/first-steps-bare.git master

Counting objects: 5, done.

Delta compression using up to 2 threads.

Compressing objects: 100% (2/2), done.

Writing objects: 100% (3/3), 293 bytes, done.

Total 3 (delta 0), reused 0 (delta 0)

Unpacking objects: 100% (3/3), done.

To /projects/first-steps-bare.git/

9e7d7b9..7e7e589 master -> master

图6 经由共享版本库来进行版本共享

3.6 Pull命令:取回修改

现在,为了让克隆版本库也得到相应的修改,我们需要在执行pull命令时配置参数指向共享版本库的路径参数。

> cd /projects/first-steps-clone

> git pull /projects/first-steps-bare.git master

remote: Counting objects: 5, done.

remote: Compressing objects: 100% (2/2), done.

remote: Total 3 (delta 0), reused 0 (delta 0)

Unpacking objects: 100% (3/3), done.

From ../first-steps-bare

* branch      master      -> FETCH_HEAD

Updating 9e7d7b9..7e7e589

Fast-forward

foo.txt |    2 +-

1 files changed, 1 insertions(+), 1 deletions(-)

请注意!如果另一个开发者在我们之前已经做过一次push操作,此次push命令就会被拒绝传送提交。这时候,我们必须要先做一次pull操作,将其他人新上载的更新取回,并在本地合并。

4 本章小结

工作区与版本库:工作区是一个包含.git子目录(内含版本库)中的目录。我们可以用init命令在当前目录中创建版本库。

版本提交:一次版本提交通常定义了版本库中所有文件的一个版本,它详细说明了该版本是由何人在何时何地创建的。当然,我们需要用add命令来确定哪些文件将被纳入下一次提交,然后再用commit命令创建新的版本提交。

查看信息:通过status命令,我们可以查看哪些文件已被本地修改,以及哪些修改将被纳入下次提交。另外,log命令可用来显示提交历史。diff命令可用来显示两个版本文件之间的差异。

克隆:对于用clone命令创建某一个版本库的副本,我们称之为该版本库的克隆体。在一般情况下,每个开发者都会拥有整个项目版本库的完整克隆体,他/她的工作区中将会包含完整的项目历史。这使他们可以各自独立开展工作,无需连接服务器。

推送与拉回:pushpull命令可用于在本地和远程版本库之间共享版本提交。

[1]译者注:示例中似乎少了用户名的部分:git config --global user.name“Hans”

本文来自《Git学习指南》

一本面向专业开发者的书

如果你在某一团队中从事开发工作,希望了解如何才能有效地使用Git,那么这本书就是一个正确的选择。本书既不是那种偏重于理论的大部头,也不是一本面面俱到的参考书。我们并不打算解释所有的Git命令(这里可有100多条命令呢)及其全部选项(有些命令甚至有50多个选项)。相反,我们打算在这本书中教你如何在典型的项目环境中使用Git,例如,如何建立起一个Git项目、如何创建一个Git发行版等。

本书相关内容

你将在本书中看到以下内容。

入门教程:这部分会重点演示每一个重要Git命令的用法,篇幅不会超过十几页。

技术介绍:在这部分不足百页的篇幅中,你将要学习如何使用Git处理一个团队开发中的各项事务。我们将会用大量的实例为你演示那些主要Git命令的使用方式。此外我们还会为你解释其中的基本概念,例如提交、版本库、分支、合并、重订等,以帮助你了解Git的具体工作方式。在这个过程中,你还会不时地看到一些相关的提示与技巧,你可能未必每天都会用到这些技巧,但它们有时还是会非常有用的。

工作流:这里的工作流主要指的是你在项目中使用Git的实用场景,例如创建一个项目的发行版等。而对于每个工作流,我们会从以下几项内来描述其目标场景。

解决的是什么问题。

需要增加什么必要条件。

解决问题的人以及解决的时间。

“解决方案选用理由”部分:每个工作流中通常都只能有一个具体的解决方案。在Git中,经常会存在着多个非常不同的解决路径,这些路径都可以让我们达成相同的目标。在每一个工作流章节的最后一部分中,我们都会详细解释为什么要选用眼下这个解决方案。另外。我们还会提一下相关的可变因素,以及我们因此可能采取的替代方案。

“分步”指令:这是一组常用命令序列,例如像移动某个分支就属于一条既定的“分步”指令。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容