版本控制工具入门 ——GIT

本文首发于 LOGI'S BLOG,由作者转载。

GIT 与 SVN 的区别

            SVN 服务器
              ↗ ↑ ↖
             /  |   \
            /   |    \
           /    |     \
         ↙      ↓      ↘
SVN 客户端  SVN 客户端  SVN 客户端

SVN 是 集中式管理版本库 位于 SVN 服务器 上,优点是便于管理员掌控 开发进度,也容易给每个开发人员 授权。缺点是,服务器可能发生 单点故障,并且 容错性较差

                共享版本库
          ——————————————————————
         /   /   ↗      \   \   ↖
        /   /   /        \   \   \
       /   /   /          \   \   \
      /   /  Push         Clone\   \
     /  Pull /              \  Pull \
 Clone  /   /                \   \  Push
  ↙    ↙   /                  ↘   ↘   \
开发人员 ——                    开发人员 ——
↑         |                   ↑         |
|         |                   |         |
——————Commit                  ——————Commit

GIT 是 分布式版本控制系统,没有中央服务器,每个开发人员都 拥有完整的版本库,开发时无需联网,修改完毕后再提交给 共享版本库 即可。

简单来说,GIT 拥有 本地仓库,而 SVN 必须连接到远程仓库修改代码。

GIT 版本控制流程图

    ———————————————Pull(Fetch + Merge)—————————————
   |                                               ↓
远程仓库  ——Clone——→  本地仓库   ————Checkout——→  工作区
 Remote  ←——Push——  Repository                Workspace
                        ↑           暂存区          |
                     Commit———————  Index  ←——————Add
                                    Stage

GIT 常用命令详解

GIT 配置

同时操作 Github 和公司私有仓库需要配置不同的邮箱和私钥;Github 配置代理才可高速访问,公司不需要。以上需求都可通过 git config 预先配置。该命令有三个作用域选项,--system--global--local,分别用来对系统,全局和项目局部进行配置,优先级由低到高,默认 --local。除了使用命令,也可直接编辑配置文件,--global 对应 $HOME/.gitconfig--local 对应项目工作目录下的 .git/config

# 查看所有配置
git config --list

用户和密钥配置

配置密钥可避免每次提交输入密码。

# 生成多个密钥,-t(type),-C(comment),-f(file)
ssh-keygen -t rsa -C "admin@gmail.com" -f ~/.ssh/id_rsa_github
ssh-keygen -t rsa -C "admin@163.com" -f ~/.ssh/id_rsa_gitlab

# 将加载密钥脚本添加到 bash 启动文件
cat >> ~/.bashrc <<"EOF"
# 启动 ssh-agent 管理 ssh session
eval $(ps -ef | grep ssh-agent | awk '{ print "kill "$2 }') 2>/dev/null
eval `ssh-agent -s` > /dev/null 2>&1
keys=(`ls ~/.ssh/*.pub | sed 's/.pub//g' | xargs`)
for key in ${keys[@]}
do
      ssh-add "${key}" > /dev/null 2>&1
done
EOF

# 重启 bash
/usr/bin/env bash
ssh -T git@github.com

# Windows 下的 Git Bash 添加密钥命令如下
exec ssh-agent bash
eval ssh-agent -s
ssh-add $HOME/.ssh/id_rsa_github

# 向全局配置文件添加不同 git 站点
cat >> $HOME/.gitconfig <<'EOF'
# gitlab
Host git.iboxpay.com
  HostName git.iboxpay.com
  PreferredAuthentications publickey
  IdentityFile ~/.ssh/id_rsa_gitlab
  User admin

# github
Host github.com
  HostName github.com
  PreferredAuthentications publickey
  IdentityFile ~/.ssh/id_rsa_github
  User admin
EOF

# 切换到每个项目目录,当独设置用户名和邮箱
git config user.email "admin@gmail.com"
git config user.name "admin"

# 取消设置
git config --unset user.name
git config --unset user.email

代理配置

为 Github 设置代理可以加快代码同步速度,对大项目很有必要。

# 设置
git config --global http.https://github.com.proxy socks5://127.0.0.1:1080

# 取消设置
git config --global --unset http.https://github.com.proxy

初始化仓库

新建文件夹 repositories/repo1 作为工作区 Working Directory/Workspace。进入工作区,通过 git initgit clone 命令创建一个本地仓库/版本库或下载远程版本库,该操作将会在工作区初始化一个 .git 文件夹,存储 版本库

# 递归创建目录
mkdir -p repositories/repo1

# 进入目录
cd repositories/repo1

# 初始化本地仓库
git init

# 不创建工作目录/工作区,用作远端共享版本库
# git init --bare

# 下载远端仓库
# git clone ssh://git@192.168.1.254/home/git/repo1

# 指定远程分支
# git clone -b dev ssh://git@192.168.1.254/home/git/repo1

提交变更

版本库中最重要的是暂存区 Stage/Index,每次在工作区 新建、修改或删除 文件后,都要使用 git add 将变更添加到暂存区,随后使用 git commit 将暂存区中的变更提交到当前 HEAD 指针指向的分支。首次 commit 时,GIT 会自动创建第一个分支 master 和指向 master 的指针 HEAD,下面是工作区和版本库的示意图。

Workspace                   |                  Repository
dir1                        |                          HEAD
  |--file1                  |                              ↘
  |--file2                  | Stage                          master
  |--file3         add——→   | dir2                           dir1
  |--dir2        ↗          |   |--file1                       |--file1
       |--file4             |   |--file2                       |--fiel2
       |--file5             |              ↘                   |--file3
                            |                 commit——→        |--dir2
                            |                                       |--file4
                            |                                       |--file5
# 新建 HelloWorld.java 文件
cat > HelloWorld.java <<"EOF"
public class HelloWorld {

    public static void main(String[] args) {
        // Prints "Hello, World" to the terminal window.
        System.out.println("Hello, World");
    }

}
EOF

# 添加单个修改文件到暂存区
git add HelloWorld.java

# 添加所有修改文件
# git add .

# 提交变更到本地仓库
git commit -m "Initial commit."

# 查看提交记录
git log --oneline

# 撤销某次之后的所有提交
git reset <commit_hash>

commit message 包含 HeaderBodyFooter 三部分,一般仅使用 Header。按照 type(scope): subject 格式填写 Header 更有助于团队合作。scope 表示功能模块,subject 代表主题,type 为类型,一般定义如下:

  • feat:新功能
  • fix:修复 bug
  • style:更改格式
  • refactor:代码重构
  • chore:项目重建
git commit -m "fix(security): upgrade lodash.template"

辅助提交

nodejs 插件 commitizen,可以帮助你填入标准的 commit message,在此之前需要安装 nodejs,可以参考 该教程

# 安装 commitizen
npm install commitizen -g

# 使用 cz-conventional-changelog 包初始化 commitizen,
#+ 需要在项目仓库运行
npm init
commitizen init cz-conventional-changelog --save-exact

# 使用 git cz 代替 git commit 提交,之后根据提示填写 message
git cz

生成 Changelog

使用 conventional-changelog 插件,可以方便地生成标准 Changelog,默认根据 commit messagefeatfix 生成。

# 安装 conventional-changelog
npm install conventional-changelog -g

# 生成 Changelog,-p(preset),
#+ -r 0 从首次 commit 开始生成,覆盖之前的 CHANGELOG.md
conventional-changelog -p angular -i CHANGELOG.md -s -r 0

文件状态追踪

仓库中的文件分为 已追踪未追踪 两种。已追踪文件在版本库中存在记录,用户工作一段时间后,这些文件仍可被查看。下面是文件状态变更图。

sequenceDiagram
    Unmodified->>Modified: Edit the file
    Modified->>Staged: Stage the file(git add)
    Unmodified->>Untracked: Remove the file
    Untracked->>Staged: Add the file
    Staged->>Unmodified: Commit

通过 git status 可以查看到最近的文件状态变更,主要意义在于提醒开发者 commit 前应先 add,之后才可以 push

# 假设之前已经 commit 过
# 新增一个文件
touch newFile

# 修改一个文件
echo 1 >> oldFile1

# 删除一个文件
rm oldFile2

# 查看文件状态变更
git status

# newFile 属于 Untracked 文件
# oldFile1 属于 Modified 文件
# oldFile2 属于 Unstaged 中的 Deleted 文件

除了查看所有文件状态,通过 git diff 还可查看当前对某个文件的具体修改。

# 查看未暂存文件修改信息
git diff

# 查看已暂存文件修改信息
git diff --staged

忽略特定文件/文件夹

在某个目录新建 .gitignore 文件,并将需要忽略的文件/文件夹写入其中,即可在 git commit 时忽略它们,该文件的语法规则如下:

  • 每行一条规则
  • 空行用于增强可读性
  • # 开头的行将被当作注释,不参与解析
  • \ 开头的规则必须使用 \\
  • 行尾的若干空格将被忽略,除非使用 \ 注释每个空格
  • ! 前缀用于否定前面的规则,但对上级目录设定的规则无效,也不会对子目录中的文件生效,如文件以 ! 开头,要使用 \! 注释
  • 在名称后添加 / 解析为文件夹,如 foo/,表示忽略当前目录的 foo/ 及其子文件夹,不忽略 foo 文件
  • 不含 / 的规则会被当作 shell glob 匹配:* 匹配除了 / 的任何字符串,? 匹配除了 / 的单个字符,[] 匹配特定范围的单个字符
  • 行首的 / 用来避免递归,如 /*.c 匹配 cat-file.c,不匹配 mozilla-sha1/sha1.c
  • 与完整路径名匹配的连续两个 ** 可能有特殊意义:
    • ** 开头后跟 / 匹配所有路径下的文件。如 **/foo 匹配任何地方的 foo 文件或路径。**/foo/bar 匹配 foo 目录下任何地方的 bar 文件或路径
    • /** 结尾的规则匹配目录下的所有文件和路径,如 abc/** 匹配 abc 及其子目录下的所有文件
    • /**/ 匹配零个或多个目录。如 a/**/b 匹配 a/ba/x/ba/x/y/b 等等

操作远程仓库

远程仓库、origin、本地仓库、暂存区和工作目录的关系。

remote repository ——-
          |         |
git fetch |         | git pull
          ↓         |
origin(remote name) |
 local repository ←--
          ↑
git commit|
          |
        index
          ↑
  git add |
          |
  working directory
# 与多个远程仓库建立连接,origin 是远程仓库别名
git remote add origin ssh://git@192.168.1.254/home/git/repo0
git remote add origin1 ssh://git@192.168.1.254/home/git/repo1

# 删除与远程仓库的连接
git remote remove origin

# 查看所有 remote 地址
git remote -v

# 将本地分支推送至远程仓库的 master 分支
#+ -u | --set-upstream,设置默认提交上游,
#+ 执行一次后,之后提交直接 git push 即可
git push -u origin master
# git push -u origin1 master

# 删除远程分支
git push origin -d dev
# git push origin1 -d dev

# 推送 tag 到远程分支
git push --tag origin dev

处理冲突

假如你的协作者和你同时拉取最新版本代码并对同一文件进行修改,当你想把变更推送至远端,而他人先于你推送时便会发生冲突,此时需要使用 git mergegit rebase 手动合并你的变更,随后才能推送。GIT 会把文件中的冲突区域标记在 <<<<<<< HEAD>>>>>>> [other/branch/name] 之间,中间用 ======= 隔开。

使用 git merge 合并时,会一次性解决之前所有提交的冲突,而 git rebase 仅解决一次提交发送的冲突,这意味着开发者之后还要执行多次 git rebase --continue 操作。

# 保存本地代码
# git stash

# 使用 merge 合并
# git fetch
# git merge
git pull

# 使用 rebase 合并
# git rebase
# git rebase --continue

# 合并本地与远程
# git stash pop

# 冲突区域示意
cat conflictFile
<<<<<<< HEAD 
int a=1;
=======
int a= 0;
>>>>>>> master

冲突处理完毕后需要将其重新添加到暂存区,再提交到本地仓库,随后提交到远程仓库。

git add .
git commit -m "refactor(Login): Merge file"
git push

管理分支

开始时,HEAD 指针指向 master 分支,master 指向最新提交,,两个指针随着 commit 不断后移如此,如此就能确定当前分支和当前提交点。

               HEAD
                ↓
              master
                ↓
◯——————◯——————◯
v1      v2      v3

创建新分支并切换

一般 master 分支用于发布新版本,dev 分支用来开发,hotfix 分支用来修复 bug。创建新分支并切换时,HEAD 指针会执行它。

# 创建分支
# git branceh dev

# 切换分支
# git checkout dev

# 可合并为一条命令
git checkout -b dev
              master
                ↓
◯——————◯——————◯
v1      v2      v3
                  ↖
                    \
                    dev
                     ↑
                    HEAD

此时,如果再进行一次提交,master 仍会停留在原位置,dev 后移指向最新提交。

# 一些修改
# ...

# 提交变更
git commit -m "refactor(Login): change code structure"
              master
                ↓
◯——————◯——————◯
v1      v2      v3
                  \
                   \
                   ◯
                  dev1
                    ↑
                   dev
                    ↑
                   HEAD

合并分支

假设开发到 dev2 时已趋于稳定,计划合并到主分支,可通过 git mergegit rebase 进行分支合并,若使用前者,两个分支在合并后都会指向公共的提交;若使用后者,将被合并分支的提将被拷贝到当前分支上,被合并分支没有提交记录。

                                       HEAD
                                        ↓
                                      master
                                        ↓
◯——————◯——————◯——————◯——————◯——————◯
v1      v2      v3      v4      v5      v6
                  \                     /
                   \                   /
                   ◯                 /
                  dev1               /
                    |               /
                   ◯———————————————
                  dev2
                    ↑
                   dev
                [git merge]
--------------------------------------------
                [git rebase]
                                               HEAD
                                                ↓
                                              master
                                                ↓
◯——————◯——————◯——————◯——————◯——————◯——————◯
v1      v2      v3      v4      v5     dev1'   dev2'
                  \
                   \
                   ◯
                  dev1
                    |
                   ◯
                  dev2
                    ↑
                   dev
# 切换回最终分支
git checkout master
# git fetch orign master
# git pull 等于 git fetch + git merge

# 将 dev 合并过来
git merge dev
# git rebase dev

# 撤销合并
git merge --abort
# git rebase --abort

# 回滚到合并前状态
# git reset --hard
# git reset <commit_hash> --hard

删除分支

# 删除
git branch -d dev

# 查看所有分支
git branch

标签管理

标签一般用作版本号,打上的标签是固定的,不像分支那样可以移动位置。

               HEAD
                ↓
              master
                ↓
◯——————◯——————◯
|       |       |
v1      v2      v3
# 查看所有标签
git tag

# 查看某些标签,-l(list)
git tag -l v0.0.*

# 为当前提交加标签,-a(add) -m(message)
git tag -am "This is the first version." v1

# 为之前某次提交打标签
# 查看提交记录
git log --oneline
# 加上 commit hash 前几位(可区分即可)
git tag 4a48e8e5f60c -am "This is the first version." v0.9

# 删除标签,-d(delete)
git tag -d v1

# 推送标签
git push --tag

撤销和回滚

可以撤销 commit 之前和之后的变更,commit 之前的变更包括未进暂存区和已进入暂存区的更改。

# 修改文件
echo "new content" >> oldFile
git status

# 未进暂存区,使用 checkout 撤销
# 撤销单个文件
git checkout --oldFile
# 撤销所有文件
git checkout
git status

##############################
echo "new content" >> oldFile
git add .
git status

# 已进入暂存区,使用 reset HEAD 将其拉出
# 拉出单个文件
git reset HEAD oldFile
# 拉出所有文件
git reset HEAD
git status


##############################
echo "new content" >> oldFile
git add .
git commit -m "refactor[Login]: add some function"
git push
git log

# 撤销已有提交,revert 实际上是一次新的 commit
git revert <commit_hash>

# 再次 revert 又可恢复提交
git revert <revert_commit_hash>
git push

当需要回滚到某次提交时,可使用 get reset 命令,与 revert 不同,该操作不会生成提交记录,因此是不可逆操作,须谨慎使用。

git reset --hard <commit_hash>
git push --force

在 IDEA 中操作 GIT

添加工程到本地仓库

  • 依次进入菜单 File -> Settings -> Version Control -> Git,配置 git.exe 路径
  • 新建项目,依次进入菜单 VCS -> Import into Version Control -> Create Git Repository,选择项目上层目录
  • 此时面板上会出现 Git 菜单,点击 Commit 图标,选择需要提交的文件和文件夹,填入 message 提交即可

远程仓库的克隆和推送

  • 依次进入菜单 File -> New -> Project from Version Contrl -> Git,添加远程仓库地址
  • 测试 ssh 方式 Windows 下不可用,使用 https 方式,输入用户名密码确定即可克隆
  • 推送时,点击 Git 菜单的 Commit,选择 Commit and Push

操作分支

  • 依次进入菜单 VCS -> Git -> Branches,选择 New BranchCheckout Tag or Revision

完整项目示例

############## 项目创建 ##############
# 创建文件夹
mkdir repo

# 进入文件夹
cd repo

# 初始化仓库
git init

# 配置用户名
git config user.name "logi"

# 配置邮箱
git config user.email "logi@logi.ml"

# 配置命令别名
git config --global alias.pull pl
git config --global alias.push ps
git config --global alias.commit cm
git config --global alias.merge mg
git config --list

# 添加 .gitignore 文件
cat > .gitignore <<'EOF'
# nodejs 相关
node_modules/
npm-debug.log*
yarn-debug.log*
npm-error.log*

# 编译后文件
/dist/

# 编辑器配置
.DS_Store
.idea
.vscode
*.suo
*.njsproj
*.sln
EOF

# 添加 README 文件
cat > README.md << 'EOF'
# Git 命令详解
EOF

# 进行首次提交
git add .
git commit -m "chore(all): initial project"

# 与远程仓库建立连接
git remote add origin ssh://git@192.168.1.254/home/git/repo

# 查看所有远程仓库
git remove -v

# 推送到远程仓库
git push -u origin master

############## 本地开发 ##############
# 建立新分支
git branch logi

# 切换到新分支
git checkout logi

# 在 logi 分支模拟开发
cat > index.js << 'EOF'
const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
EOF

# 查看最近变更
git status

# 将变更加入暂存区
git add .
# 提交变更
git commit -m "feat(index): add project index"
# 推送到远程仓库
git push -u origin logi


############## 项目上线 ##############
# 切换到 master 分支
git checkout master

# 合并 logi 分支
git merge logi

# 推送到远端 master 分支
git push -u origin master

# 打 tag
git tag -a v0.1.0 -m "First version"

# 推送 tag 到远程分支
git push --tag origin master

############## 问题修复 ##############
# 建立新分支并切换
git checkout -b hotfix-718

# 模拟修复过程
echo >> index.js <<'EOF'
function hotfix718() { //... }
hotfix718;
EOF

# 提交代码
git add .
git commit -m "fix(index): fix ui bugs"
git push -u origin hotfix-415

# 切换回 master 分支
git checkout master
git merge hotifix-718
git push

# 打上新版本并提交
git tag -a v0.1.1 -m "Fix bugs"
git push --tag origin master

# 删除 hotfix 分支
git branch -d hotfix-718
git push origin -d hotfix-718

############## 继续开发 ##############
# 切换到个人分支 logi
git checkout logi

# 修改某个模块
echo > index.js <<'EOF'
// ...
EOF

# 发现忘记拉取 master 最新代码,
#+ 此时需要保存工作
git stash

# 拉取最新代码
git pull origin master

# 恢复暂存文件,之后可能需要处理冲突
git stash pop

# 提交修改
git add .
git commit -m "feat(index): add auth module"
git push

# 合并到 master 并推送
git checkout master
git merge logi
git push origin master

# 打上新版本并提交
git tag -a v0.1.2 -m "Fix bugs"
git push

参考文献

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

推荐阅读更多精彩内容

  • 来源:Git由浅入深之操作与指令作者:惊鸿三世(转载已获得原作者许可,如需转载请与原作者联系) 本篇正式开始介绍G...
    极乐君阅读 1,632评论 9 67
  • 一、基本概念: 注:对于git的分布式概念及其优点,不重复说明,自己百度或谷歌。本文中涉及到指令前面有$的,在cm...
    大厂offer阅读 1,421评论 0 3
  • git 使用笔记 git原理: 文件(blob)对象,树(tree)对象,提交(commit)对象 tree对象 ...
    神刀阅读 3,767评论 0 10
  • 安装Git Git的下载地址:Git官网下载地址 Git本地仓库和命令 配置用户 下载完Git后,右键会有一个Gi...
    TokyoZ阅读 4,497评论 1 7
  • @外滩源壹号 中山东一路33号。原英国驻沪总领事馆。外滩现存最早的建筑。同治十一年建造,英国文艺复兴风格。 偷得浮...
    十七楼的安素阅读 624评论 11 12